├── .github ├── renovate.json └── workflows │ ├── ci.yml │ └── deploy.yml ├── .gitignore ├── LICENSE ├── README.md ├── eslint.config.mjs ├── examples ├── animation.html ├── annotations.html ├── ar-avatar.html ├── ar-hand-gestures.html ├── assets │ ├── fonts │ │ ├── arial.json │ │ ├── arial.png │ │ ├── arial.ttf │ │ ├── courier.json │ │ ├── courier.png │ │ ├── trade gothic lt bold no. 2.json │ │ └── trade gothic lt bold no. 2.png │ ├── models │ │ ├── fps-map.glb │ │ ├── fps-map.txt │ │ ├── jet-fighter.glb │ │ ├── jet-fighter.txt │ │ ├── nyan-cat.glb │ │ ├── planets │ │ │ ├── earth.glb │ │ │ ├── jupiter.glb │ │ │ ├── mars.glb │ │ │ ├── mercury.glb │ │ │ ├── neptune.glb │ │ │ ├── saturn.glb │ │ │ ├── sun.glb │ │ │ ├── uranus.glb │ │ │ └── venus.glb │ │ ├── playcanvas-cube.glb │ │ ├── porsche-911-carrera-4s.glb │ │ ├── porsche-911-carrera-4s.txt │ │ ├── raccoon-head.glb │ │ ├── rounded-box.glb │ │ ├── shoe.glb │ │ ├── shoe.txt │ │ ├── star.glb │ │ ├── star.txt │ │ ├── t-rex.glb │ │ ├── vintage-pc.glb │ │ ├── vintage-pc.txt │ │ └── walking-robot.glb │ ├── particles │ │ ├── snow.json │ │ ├── snowflake.png │ │ ├── spark.png │ │ └── sparks.json │ ├── scripts │ │ ├── annotation.mjs │ │ ├── camera-feed.mjs │ │ ├── choose-color.mjs │ │ ├── face-detection.mjs │ │ ├── falling-blocks.mjs │ │ ├── follow-pointer.mjs │ │ ├── gravity.mjs │ │ ├── hand-gestures.mjs │ │ ├── material-variants.mjs │ │ ├── morph-update.mjs │ │ ├── orbit.mjs │ │ ├── rotate.mjs │ │ ├── scroll.mjs │ │ ├── solar-system.mjs │ │ ├── static-body.mjs │ │ ├── text3d.mjs │ │ ├── tweener.mjs │ │ ├── video-recorder-ui.mjs │ │ ├── video-recorder.mjs │ │ └── video-texture.mjs │ ├── skies │ │ ├── autumn-field-puresky.webp │ │ ├── dry-lake-bed-2k.hdr │ │ ├── octagon-lamps-photo-studio-2k.hdr │ │ ├── sepulchral-chapel-rotunda-4k.webp │ │ ├── shanghai-riverside-4k.hdr │ │ └── stars.png │ ├── sounds │ │ ├── clear1.mp3 │ │ ├── clear4.mp3 │ │ ├── drop.mp3 │ │ ├── footsteps.mp3 │ │ ├── gameover.mp3 │ │ ├── nyan-cat.mp3 │ │ └── rotate.mp3 │ ├── splats │ │ ├── angel.compressed.ply │ │ └── biker.compressed.ply │ ├── textures │ │ ├── metal-diffuse.jpg │ │ ├── metal-metalness.jpg │ │ ├── metal-normal.jpg │ │ └── metal-roughness.jpg │ └── videos │ │ └── doom.mp4 ├── basic-particles.html ├── basic-shapes.html ├── car-configurator.html ├── css │ ├── browser.css │ └── example.css ├── fps-controller.html ├── glb.html ├── img │ ├── playcanvas-192.png │ └── playcanvas.png ├── index.html ├── js │ ├── browser.mjs │ ├── example-list.mjs │ ├── example.mjs │ ├── navigation.mjs │ └── qr-code.mjs ├── manifest.json ├── modules │ ├── ammo │ │ ├── ammo.js │ │ ├── ammo.wasm.js │ │ └── ammo.wasm.wasm │ ├── basis │ │ ├── basis.js │ │ ├── basis.wasm.js │ │ └── basis.wasm.wasm │ └── draco │ │ ├── draco.js │ │ ├── draco.wasm.js │ │ └── draco.wasm.wasm ├── physics-cluster.html ├── physics.html ├── positional-sound.html ├── screen.html ├── shoe-configurator.html ├── solar-system.html ├── sound.html ├── spinning-cube-api.html ├── spinning-cube-umd.html ├── spinning-cube.html ├── splat.html ├── text.html ├── text3d.html ├── tween.html ├── vibe-falling-blocks.html ├── video-recorder.html └── video-texture.html ├── lib ├── meshopt_decoder.module.d.ts └── meshopt_decoder.module.js ├── package-lock.json ├── package.json ├── rollup.config.mjs ├── src ├── app.ts ├── asset.ts ├── async-element.ts ├── colors.ts ├── components │ ├── camera-component.ts │ ├── collision-component.ts │ ├── component.ts │ ├── element-component.ts │ ├── light-component.ts │ ├── listener-component.ts │ ├── particlesystem-component.ts │ ├── render-component.ts │ ├── rigidbody-component.ts │ ├── screen-component.ts │ ├── script-component.ts │ ├── script.ts │ ├── sound-component.ts │ ├── sound-slot.ts │ └── splat-component.ts ├── entity.ts ├── index.ts ├── material.ts ├── model.ts ├── module.ts ├── scene.ts ├── sky.ts └── utils.ts ├── tsconfig.json ├── typedoc.json └── utils └── typedoc └── favicon.ico /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "packageRules": [ 7 | { 8 | "matchManagers": [ 9 | "npm" 10 | ], 11 | "groupName": "all npm dependencies", 12 | "schedule": [ 13 | "on monday at 10:00am" 14 | ] 15 | }, 16 | { 17 | "matchDepTypes": ["devDependencies"], 18 | "rangeStrategy": "pin" 19 | }, 20 | { 21 | "matchDepTypes": ["dependencies"], 22 | "rangeStrategy": "widen" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: '22' 20 | 21 | - name: Install dependencies 22 | run: npm ci 23 | 24 | - name: Build 25 | run: npm run build 26 | 27 | lint: 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | 33 | - name: Setup Node.js 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: '22' 37 | 38 | - name: Install dependencies 39 | run: npm ci 40 | 41 | - name: Run lint 42 | run: npm run lint 43 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: "Deploy" 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main] 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | # Allow only one concurrent deployment 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | deploy: 20 | runs-on: ubuntu-latest 21 | environment: 22 | name: github-pages 23 | url: ${{ steps.deployment.outputs.page_url }} 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: '22' 32 | 33 | - name: Install dependencies 34 | run: npm ci 35 | 36 | - name: Build 37 | run: npm run build 38 | 39 | - name: Setup Pages 40 | uses: actions/configure-pages@v5 41 | 42 | - name: Upload artifact 43 | uses: actions/upload-pages-artifact@v3 44 | with: 45 | path: '.' 46 | 47 | - name: Deploy to GitHub Pages 48 | id: deployment 49 | uses: actions/deploy-pages@v4 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Npm rc config file 55 | .npmrc 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional stylelint cache 61 | .stylelintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variable files 79 | .env 80 | .env.development.local 81 | .env.test.local 82 | .env.production.local 83 | .env.local 84 | 85 | # parcel-bundler cache (https://parceljs.org/) 86 | .cache 87 | .parcel-cache 88 | 89 | # Next.js build output 90 | .next 91 | out 92 | 93 | # Nuxt.js build / generate output 94 | .nuxt 95 | dist 96 | 97 | # Gatsby files 98 | .cache/ 99 | # Comment in the public line in if your project uses Gatsby and not Next.js 100 | # https://nextjs.org/blog/next-9-1#public-directory-support 101 | # public 102 | 103 | # vuepress build output 104 | .vuepress/dist 105 | 106 | # vuepress v2.x temp and cache directory 107 | .temp 108 | .cache 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | docs 136 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2011-2025 PlayCanvas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PlayCanvas Web Components 2 | 3 | [![NPM Version](https://img.shields.io/npm/v/@playcanvas/web-components.svg)](https://www.npmjs.com/package/@playcanvas/web-components) 4 | [![NPM Downloads](https://img.shields.io/npm/dw/@playcanvas/web-components)](https://npmtrends.com/@playcanvas/web-components) 5 | [![License](https://img.shields.io/npm/l/@playcanvas/web-components.svg)](https://github.com/playcanvas/web-components/blob/main/LICENSE) 6 | [![GitHub Actions Build Status](https://github.com/playcanvas/web-components/actions/workflows/deploy.yml/badge.svg)](https://github.com/playcanvas/web-components/actions/workflows/deploy.yml) 7 | 8 | | [User Guide](https://developer.playcanvas.com/user-manual/web-components) | [API Reference](https://api.playcanvas.com/web-components/) | [Examples](https://playcanvas.github.io/web-components/examples) | [Blog](https://blog.playcanvas.com/) | [Forum](https://forum.playcanvas.com/) | [Discord](https://discord.gg/RSaMRzg) | 9 | 10 | PlayCanvas Web Components are a set of custom HTML elements for building 3D interactive web apps. Using the declarative nature of HTML makes it both easy and fun to incorporate 3D into your website. Check out this simple example: 11 | 12 | ```html 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ``` 28 | 29 | ## Examples 30 | 31 | [![image](https://github.com/user-attachments/assets/25ac8dd3-abc9-4d65-8950-3d72ed1f7152)](https://playcanvas.github.io/web-components/examples) 32 | 33 | See PlayCanvas Web Components in action here: https://playcanvas.github.io/web-components/examples 34 | 35 | ## Usage 36 | 37 | Please see the [Getting Started Guide](https://developer.playcanvas.com/user-manual/engine/web-components/getting-started) for installation and usage instructions. 38 | 39 | ## Development 40 | 41 | ### Setting Up Local Development 42 | 43 | 1. Clone the repository: 44 | 45 | ```bash 46 | git clone https://github.com/playcanvas/web-components.git 47 | cd web-components 48 | ``` 49 | 50 | 2. Install dependencies: 51 | 52 | ```bash 53 | npm install 54 | ``` 55 | 56 | 3. Build the library in watch mode and start the development server: 57 | 58 | ```bash 59 | npm run dev 60 | ``` 61 | 62 | 4. Open http://localhost:3000/examples/ in your browser to see the examples. 63 | 64 | ### Building 65 | 66 | To build the library: 67 | 68 | ```bash 69 | npm run build 70 | ``` 71 | 72 | The built files will be available in the `dist` directory. 73 | 74 | ### API Documentation 75 | 76 | To generate API documentation: 77 | 78 | ```bash 79 | npm run docs 80 | ``` 81 | 82 | The documentation will be generated in the `docs` directory. 83 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import playcanvasConfig from '@playcanvas/eslint-config'; 2 | import tsParser from '@typescript-eslint/parser'; 3 | import tsPlugin from '@typescript-eslint/eslint-plugin'; 4 | import globals from 'globals'; 5 | 6 | export default [ 7 | ...playcanvasConfig, 8 | { 9 | files: ['examples/js/**/*.mjs', 'examples/assets/scripts/**/*.mjs'], 10 | languageOptions: { 11 | globals: { 12 | ...globals.browser 13 | } 14 | } 15 | }, 16 | { 17 | files: ['**/*.ts'], 18 | languageOptions: { 19 | parser: tsParser, 20 | globals: { 21 | ...globals.browser, 22 | AddEventListenerOptions: "readonly", 23 | EventListener: "readonly", 24 | EventListenerOptions: "readonly" 25 | } 26 | }, 27 | plugins: { 28 | '@typescript-eslint': tsPlugin 29 | }, 30 | settings: { 31 | 'import/resolver': { 32 | typescript: {} 33 | } 34 | }, 35 | rules: { 36 | ...tsPlugin.configs['recommended'].rules, 37 | '@typescript-eslint/ban-ts-comment': 'off', 38 | '@typescript-eslint/no-explicit-any': 'off', 39 | '@typescript-eslint/no-unused-vars': 'off', 40 | 'jsdoc/require-param-type': 'off', 41 | 'jsdoc/require-returns-type': 'off' 42 | } 43 | } 44 | ]; 45 | -------------------------------------------------------------------------------- /examples/animation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - GLB Animation 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /examples/ar-avatar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - AR Avatar 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/ar-hand-gestures.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - AR Hand Gestures 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/assets/fonts/arial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/fonts/arial.png -------------------------------------------------------------------------------- /examples/assets/fonts/arial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/fonts/arial.ttf -------------------------------------------------------------------------------- /examples/assets/fonts/courier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/fonts/courier.png -------------------------------------------------------------------------------- /examples/assets/fonts/trade gothic lt bold no. 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/fonts/trade gothic lt bold no. 2.png -------------------------------------------------------------------------------- /examples/assets/models/fps-map.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/fps-map.glb -------------------------------------------------------------------------------- /examples/assets/models/fps-map.txt: -------------------------------------------------------------------------------- 1 | The low poly fps tdm game map model has been obtained from this address: 2 | https://sketchfab.com/3d-models/de-dust-2-with-real-light-4ce74cd95c584ce9b12b5ed9dc418db5 3 | It's distributed under CC license: 4 | https://creativecommons.org/licenses/by/4.0/ 5 | -------------------------------------------------------------------------------- /examples/assets/models/jet-fighter.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/jet-fighter.glb -------------------------------------------------------------------------------- /examples/assets/models/jet-fighter.txt: -------------------------------------------------------------------------------- 1 | Mitsubishi F-2 - Fighter Jet - Free by bohmerang on Sketchfab: 2 | 3 | https://sketchfab.com/3d-models/mitsubishi-f-2-fighter-jet-free-d3d7244554974f499b106e6c11fe3aaf 4 | 5 | CC BY 4.0 https://creativecommons.org/licenses/by/4.0/ 6 | -------------------------------------------------------------------------------- /examples/assets/models/nyan-cat.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/nyan-cat.glb -------------------------------------------------------------------------------- /examples/assets/models/planets/earth.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/planets/earth.glb -------------------------------------------------------------------------------- /examples/assets/models/planets/jupiter.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/planets/jupiter.glb -------------------------------------------------------------------------------- /examples/assets/models/planets/mars.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/planets/mars.glb -------------------------------------------------------------------------------- /examples/assets/models/planets/mercury.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/planets/mercury.glb -------------------------------------------------------------------------------- /examples/assets/models/planets/neptune.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/planets/neptune.glb -------------------------------------------------------------------------------- /examples/assets/models/planets/saturn.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/planets/saturn.glb -------------------------------------------------------------------------------- /examples/assets/models/planets/sun.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/planets/sun.glb -------------------------------------------------------------------------------- /examples/assets/models/planets/uranus.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/planets/uranus.glb -------------------------------------------------------------------------------- /examples/assets/models/planets/venus.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/planets/venus.glb -------------------------------------------------------------------------------- /examples/assets/models/playcanvas-cube.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/playcanvas-cube.glb -------------------------------------------------------------------------------- /examples/assets/models/porsche-911-carrera-4s.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/porsche-911-carrera-4s.glb -------------------------------------------------------------------------------- /examples/assets/models/porsche-911-carrera-4s.txt: -------------------------------------------------------------------------------- 1 | (FREE) Porsche 911 Carrera 4S by Lionsharp Studios on Sketchfab: 2 | 3 | https://sketchfab.com/3d-models/free-porsche-911-carrera-4s-d01b254483794de3819786d93e0e1ebf 4 | 5 | CC BY 4.0 https://creativecommons.org/licenses/by/4.0/ 6 | -------------------------------------------------------------------------------- /examples/assets/models/raccoon-head.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/raccoon-head.glb -------------------------------------------------------------------------------- /examples/assets/models/rounded-box.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/rounded-box.glb -------------------------------------------------------------------------------- /examples/assets/models/shoe.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/shoe.glb -------------------------------------------------------------------------------- /examples/assets/models/shoe.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 Shopify, Inc. 2 | CC BY 4.0 https://creativecommons.org/licenses/by/4.0/ 3 | -------------------------------------------------------------------------------- /examples/assets/models/star.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/star.glb -------------------------------------------------------------------------------- /examples/assets/models/star.txt: -------------------------------------------------------------------------------- 1 | Cute little Star by totomori on Sketchfab: 2 | 3 | https://sketchfab.com/3d-models/cute-little-star-1fc3bdccaad9455db5a9ed80f5a61cb9 4 | 5 | CC BY 4.0 https://creativecommons.org/licenses/by/4.0/ 6 | -------------------------------------------------------------------------------- /examples/assets/models/t-rex.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/t-rex.glb -------------------------------------------------------------------------------- /examples/assets/models/vintage-pc.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/vintage-pc.glb -------------------------------------------------------------------------------- /examples/assets/models/vintage-pc.txt: -------------------------------------------------------------------------------- 1 | PC-9801UX by darekagomi on Sketchfab: 2 | 3 | https://sketchfab.com/3d-models/pc-9801ux-2befdff3817c4b1f86373149f3328e2f 4 | 5 | CC BY 4.0 https://creativecommons.org/licenses/by/4.0/ 6 | -------------------------------------------------------------------------------- /examples/assets/models/walking-robot.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/models/walking-robot.glb -------------------------------------------------------------------------------- /examples/assets/particles/snow.json: -------------------------------------------------------------------------------- 1 | { 2 | "numParticles": 100, 3 | "lifetime": 10, 4 | "rate": 0.1, 5 | "colorMapAsset": "snowflake", 6 | "emitterExtents": [ 15, 0, 10 ], 7 | "startAngle": 360, 8 | "startAngle2": -360, 9 | "alphaGraph": { 10 | "keys": [ 0, 0, 0.5, 0.5, 0.9, 0.9, 1, 0 ] 11 | }, 12 | "rotationSpeedGraph": { 13 | "keys": [ 0, 100 ] 14 | }, 15 | "rotationSpeedGraph2": { 16 | "keys": [ 0, -100 ] 17 | }, 18 | "scaleGraph": { 19 | "keys": [ 0, 0.1 ] 20 | }, 21 | "velocityGraph": { 22 | "keys": [ 23 | [ 0, 0 ], 24 | [ 0, -0.7 ], 25 | [ 0, 0 ] 26 | ] 27 | }, 28 | "velocityGraph2": { 29 | "keys": [ 30 | [ 0, 0 ], 31 | [ 0, -0.4 ], 32 | [ 0, 0 ] 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/assets/particles/snowflake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/particles/snowflake.png -------------------------------------------------------------------------------- /examples/assets/particles/spark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/particles/spark.png -------------------------------------------------------------------------------- /examples/assets/particles/sparks.json: -------------------------------------------------------------------------------- 1 | { 2 | "numParticles": 200, 3 | "lifetime": 2, 4 | "rate": 0.01, 5 | "colorMapAsset": "spark", 6 | "colorGraph": { 7 | "keys": [ 8 | [0, 1, 0.25, 1, 0.375, 0.5, 0.5, 0], 9 | [0, 0, 0.125, 0.25, 0.25, 0.5, 0.375, 0.75, 0.5, 1], 10 | [0, 0, 1, 0] 11 | ] 12 | }, 13 | "localVelocityGraph": { 14 | "keys": [ 15 | [0, 0, 1, 8], 16 | [0, 0, 1, 6], 17 | [0, 0, 1, 0] 18 | ] 19 | }, 20 | "localVelocityGraph2": { 21 | "keys": [ 22 | [0, 0, 1, -8], 23 | [0, 0, 1, -6], 24 | [0, 0, 1, 0] 25 | ] 26 | }, 27 | "rotationSpeedGraph": { 28 | "keys": [0, 360] 29 | }, 30 | "scaleGraph": { 31 | "keys": [0, 0, 0.5, 0.3, 0.8, 0.2, 1, 0.1] 32 | }, 33 | "velocityGraph": { 34 | "keys": [ 35 | [0, 0], 36 | [0, 0, 0.2, 6, 1, -48], 37 | [0, 0] 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/assets/scripts/camera-feed.mjs: -------------------------------------------------------------------------------- 1 | import { Script } from 'playcanvas'; 2 | 3 | /** 4 | * A script that displays a live camera feed from the device's camera. 5 | * 6 | * This script creates a video element and requests access to the device's camera. 7 | * It then streams the camera's video to the video element and plays it. 8 | */ 9 | export class CameraFeed extends Script { 10 | /** 11 | * Whether to flip the video stream horizontally to behave like a mirror. 12 | * 13 | * @type {boolean} 14 | * @attribute 15 | */ 16 | mirror = true; 17 | 18 | /** 19 | * @type {HTMLVideoElement|null} 20 | */ 21 | video = null; 22 | 23 | createVideoElement() { 24 | const video = document.createElement('video'); 25 | 26 | // Enable inline playback, autoplay, and mute (important for mobile) 27 | video.setAttribute('playsinline', ''); 28 | video.autoplay = true; 29 | video.muted = true; 30 | 31 | // Style the video element to fill the viewport and cover it like CSS background-size: cover 32 | video.style.position = 'absolute'; 33 | video.style.top = '0'; 34 | video.style.left = '0'; 35 | video.style.width = '100%'; 36 | video.style.height = '100%'; 37 | video.style.objectFit = 'cover'; 38 | 39 | // Set a negative z-index so the video appears behind the canvas 40 | video.style.zIndex = '-1'; 41 | 42 | // Mirror the video stream, if chosen. 43 | if (this.mirror) { 44 | video.style.transform = 'scaleX(-1)'; 45 | } 46 | 47 | return video; 48 | } 49 | 50 | initialize() { 51 | this.video = this.createVideoElement(); 52 | 53 | // Insert the video element into the DOM. 54 | document.body.appendChild(this.video); 55 | 56 | this.on('destroy', () => { 57 | if (this.video && this.video.srcObject) { 58 | // Stop the video stream 59 | this.video.srcObject.getTracks().forEach(track => track.stop()); 60 | } 61 | // Remove the video element from the DOM 62 | document.body.removeChild(this.video); 63 | this.video = null; 64 | }); 65 | 66 | // Request access to the webcam 67 | if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { 68 | navigator.mediaDevices.getUserMedia({ video: true, audio: false }) 69 | .then((stream) => { 70 | // Stream the webcam to the video element and play it 71 | this.video.srcObject = stream; 72 | this.video.play(); 73 | }) 74 | .catch((error) => { 75 | console.error('Error accessing the webcam:', error); 76 | }); 77 | } else { 78 | console.error('getUserMedia is not supported in this browser.'); 79 | } 80 | } 81 | 82 | update(dt) { 83 | // Optional per-frame logic. 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /examples/assets/scripts/choose-color.mjs: -------------------------------------------------------------------------------- 1 | import { math, Script, Color } from 'playcanvas'; 2 | 3 | export class ChooseColor extends Script { 4 | // Define available colors as a static property 5 | static PAINT_OPTIONS = [ 6 | { name: 'Guards Red', color: new Color(0.902, 0.004, 0.086), metallic: false }, 7 | { name: 'Racing Yellow', color: new Color(1, 0.831, 0), metallic: false }, 8 | { name: 'GT Silver', color: new Color(0.82, 0.82, 0.82), metallic: true }, 9 | { name: 'Jet Black', color: new Color(0.05, 0.05, 0.05), metallic: true }, 10 | { name: 'Carrara White', color: new Color(0.95, 0.95, 0.95), metallic: false }, 11 | { name: 'Gentian Blue', color: new Color(0.15, 0.24, 0.41), metallic: true }, 12 | { name: 'Agate Grey', color: new Color(0.47, 0.47, 0.47), metallic: true }, 13 | { name: 'Shark Blue', color: new Color(0.16, 0.33, 0.47), metallic: true }, 14 | { name: 'Python Green', color: new Color(0.38, 0.45, 0.23), metallic: true }, 15 | { name: 'Miami Blue', color: new Color(0, 0.67, 0.87), metallic: false } 16 | ]; 17 | 18 | // Constants 19 | static TRANSITION_SPEED = 2; // 0.5 seconds transition 20 | 21 | static METALNESS = { 22 | METALLIC: 0.9, 23 | NON_METALLIC: 0 24 | }; 25 | 26 | // Initialize properties 27 | /** 28 | * @type {import('playcanvas').StandardMaterial} 29 | */ 30 | material = null; 31 | 32 | fromColor = new Color(); 33 | 34 | toColor = new Color(); 35 | 36 | fromMetalness = 0; 37 | 38 | toMetalness = 0; 39 | 40 | lerpTime = 0; 41 | 42 | isTransitioning = false; 43 | 44 | initialize() { 45 | this.findMaterial(); 46 | this.createUI(); 47 | } 48 | 49 | findMaterial() { 50 | for (const render of this.entity.findComponents('render')) { 51 | for (const meshInstance of render.meshInstances) { 52 | if (meshInstance.material.name === 'coat') { 53 | this.material = meshInstance.material; 54 | this.fromColor.copy(this.material.diffuse); 55 | this.toColor.copy(this.material.diffuse); 56 | this.fromMetalness = this.material.metalness; 57 | this.toMetalness = this.material.metalness; 58 | return; 59 | } 60 | } 61 | } 62 | } 63 | 64 | createUI() { 65 | const container = this.createContainer(); 66 | ChooseColor.PAINT_OPTIONS.forEach((color) => { 67 | container.appendChild(this.createColorButton(color)); 68 | }); 69 | document.body.appendChild(container); 70 | } 71 | 72 | createContainer() { 73 | const container = document.createElement('div'); 74 | container.classList.add('example-button-container', 'top-right'); 75 | container.style.display = 'grid'; 76 | container.style.gridTemplateColumns = 'repeat(5, 1fr)'; 77 | container.style.gap = '8px'; 78 | return container; 79 | } 80 | 81 | createColorButton(color) { 82 | const button = document.createElement('button'); 83 | button.title = color.name; 84 | Object.assign(button.style, { 85 | width: '40px', 86 | height: '40px', 87 | border: '2px solid black', 88 | backgroundColor: color.color.toString(), 89 | cursor: 'pointer', 90 | padding: '0', 91 | borderRadius: '4px' 92 | }); 93 | button.onclick = () => this.startColorTransition(color); 94 | return button; 95 | } 96 | 97 | startColorTransition(color) { 98 | if (!this.material) return; 99 | 100 | this.fromColor.copy(this.material.diffuse); 101 | this.toColor.copy(color.color); 102 | this.fromMetalness = this.material.metalness; 103 | this.toMetalness = color.metallic ? 104 | ChooseColor.METALNESS.METALLIC : 105 | ChooseColor.METALNESS.NON_METALLIC; 106 | this.lerpTime = 0; 107 | this.isTransitioning = true; 108 | } 109 | 110 | update(dt) { 111 | if (!this.isTransitioning || !this.material) return; 112 | 113 | this.lerpTime += dt * ChooseColor.TRANSITION_SPEED; 114 | const t = Math.min(this.lerpTime, 1); 115 | 116 | this.updateMaterial(t); 117 | 118 | if (t >= 1) { 119 | this.isTransitioning = false; 120 | } 121 | } 122 | 123 | updateMaterial(t) { 124 | this.material.diffuse.lerp(this.fromColor, this.toColor, t); 125 | this.material.metalness = math.lerp(this.fromMetalness, this.toMetalness, t); 126 | this.material.clearCoat = 0.25; 127 | this.material.clearCoatGloss = 0.9; 128 | this.material.update(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /examples/assets/scripts/face-detection.mjs: -------------------------------------------------------------------------------- 1 | import { FaceLandmarker, FilesetResolver } from '@mediapipe/tasks-vision'; 2 | import { Mat4, Script } from 'playcanvas'; 3 | 4 | export class FaceDetection extends Script { 5 | /** 6 | * @type {FaceLandmarker} 7 | * @private 8 | */ 9 | faceLandmarker; 10 | 11 | /** 12 | * @type {boolean} 13 | * @private 14 | */ 15 | mirror = true; 16 | 17 | /** 18 | * @type {HTMLCanvasElement} 19 | * @private 20 | */ 21 | offscreenCanvas = null; 22 | 23 | /** 24 | * @type {CanvasRenderingContext2D} 25 | * @private 26 | */ 27 | offscreenCtx = null; 28 | 29 | async initialize() { 30 | const wasmFileset = await FilesetResolver.forVisionTasks( 31 | '../node_modules/@mediapipe/tasks-vision/wasm' 32 | ); 33 | this.faceLandmarker = await FaceLandmarker.createFromOptions(wasmFileset, { 34 | baseOptions: { 35 | modelAssetPath: 'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task', 36 | delegate: 'GPU' 37 | }, 38 | outputFaceBlendshapes: true, 39 | outputFacialTransformationMatrixes: true, 40 | runningMode: 'VIDEO', 41 | numFaces: 1 42 | }); 43 | } 44 | 45 | update(dt) { 46 | if (!this.faceLandmarker) return; 47 | 48 | const video = document.querySelector('video'); 49 | 50 | // Only process if the video has enough data. 51 | if (!video || video.readyState < HTMLMediaElement.HAVE_ENOUGH_DATA) return; 52 | 53 | let inputElement = video; 54 | 55 | // If we want the detection to work in the mirrored space, 56 | // draw the video frame into an off-screen canvas that flips it. 57 | if (this.mirror) { 58 | if (!this.offscreenCanvas) { 59 | this.offscreenCanvas = document.createElement('canvas'); 60 | this.offscreenCtx = this.offscreenCanvas.getContext('2d'); 61 | } 62 | // Update canvas dimensions (in case they change). 63 | this.offscreenCanvas.width = video.videoWidth; 64 | this.offscreenCanvas.height = video.videoHeight; 65 | 66 | // Draw the video frame flipped horizontally. 67 | this.offscreenCtx.save(); 68 | this.offscreenCtx.scale(-1, 1); 69 | // Drawing at negative width flips the image. 70 | this.offscreenCtx.drawImage(video, -video.videoWidth, 0, video.videoWidth, video.videoHeight); 71 | this.offscreenCtx.restore(); 72 | 73 | // Feed the flipped image to MediaPipe. 74 | inputElement = this.offscreenCanvas; 75 | } 76 | 77 | const detections = this.faceLandmarker.detectForVideo(inputElement, Date.now()); 78 | 79 | if (!detections) return; 80 | 81 | // Process facial transformation matrix 82 | if (detections.facialTransformationMatrixes && detections.facialTransformationMatrixes.length > 0) { 83 | // Apply head transform using facial transformation matrix 84 | const { data } = detections.facialTransformationMatrixes[0]; 85 | const matrix = new Mat4(); 86 | matrix.set(data).invert(); 87 | const position = matrix.getTranslation(); 88 | const rotation = matrix.getEulerAngles(); 89 | this.entity.setPosition(position); 90 | this.entity.setEulerAngles(rotation); 91 | } 92 | 93 | // Process blendshapes 94 | if (detections.faceBlendshapes && detections.faceBlendshapes.length > 0) { 95 | const { categories } = detections.faceBlendshapes[0]; 96 | this.app.fire('face:blendshapes', categories); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /examples/assets/scripts/follow-pointer.mjs: -------------------------------------------------------------------------------- 1 | import { Script } from 'playcanvas'; 2 | 3 | /** 4 | * @import { CameraComponent } from 'playcanvas'; 5 | */ 6 | 7 | export class FollowPointer extends Script { 8 | initialize() { 9 | const canvas = this.app.graphicsDevice.canvas; 10 | canvas.addEventListener('pointermove', (event) => { 11 | 12 | /** @type {CameraComponent} */ 13 | const camera = this.app.root.findComponent('camera'); 14 | const { z } = camera.entity.getPosition(); 15 | const { x, y } = camera.screenToWorld(event.clientX, event.clientY, z); 16 | this.entity.setPosition(x, y, 0); 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/assets/scripts/gravity.mjs: -------------------------------------------------------------------------------- 1 | import { Script } from 'playcanvas'; 2 | 3 | export class Gravity extends Script { 4 | update(dt) { 5 | const { x, y, z } = this.entity.getPosition(); 6 | this.entity.rigidbody.applyForce(-x, -y, -z); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/assets/scripts/hand-gestures.mjs: -------------------------------------------------------------------------------- 1 | import { HandLandmarker, GestureRecognizer, FilesetResolver } from '@mediapipe/tasks-vision'; 2 | import { Script, Vec3 } from 'playcanvas'; 3 | 4 | export class HandGestureController extends Script { 5 | /** 6 | * Maximum number of hands to detect 7 | * @type {number} 8 | * @attribute 9 | */ 10 | maxNumHands = 2; 11 | 12 | /** 13 | * Whether to mirror the input 14 | * @type {boolean} 15 | * @attribute 16 | */ 17 | mirror = true; 18 | 19 | /** 20 | * @type {HandLandmarker} 21 | * @private 22 | */ 23 | handLandmarker; 24 | 25 | /** 26 | * @type {GestureRecognizer} 27 | * @private 28 | */ 29 | gestureRecognizer; 30 | 31 | /** 32 | * @type {HTMLCanvasElement} 33 | * @private 34 | */ 35 | offscreenCanvas = null; 36 | 37 | async initialize() { 38 | const wasmFileset = await FilesetResolver.forVisionTasks( 39 | '../node_modules/@mediapipe/tasks-vision/wasm' 40 | ); 41 | 42 | // Initialize hand landmarker 43 | this.handLandmarker = await HandLandmarker.createFromOptions(wasmFileset, { 44 | baseOptions: { 45 | modelAssetPath: 'https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task', 46 | delegate: 'GPU' 47 | }, 48 | numHands: this.maxNumHands, 49 | runningMode: 'VIDEO' 50 | }); 51 | 52 | // Initialize gesture recognizer 53 | this.gestureRecognizer = await GestureRecognizer.createFromOptions(wasmFileset, { 54 | baseOptions: { 55 | modelAssetPath: 'https://storage.googleapis.com/mediapipe-models/gesture_recognizer/gesture_recognizer/float16/1/gesture_recognizer.task', 56 | delegate: 'GPU' 57 | }, 58 | runningMode: 'VIDEO' 59 | }); 60 | } 61 | 62 | update(dt) { 63 | if (!this.handLandmarker || !this.gestureRecognizer) return; 64 | 65 | const video = document.querySelector('video'); 66 | if (!video || video.readyState < HTMLMediaElement.HAVE_ENOUGH_DATA) return; 67 | 68 | const inputElement = this.prepareInputElement(video); 69 | 70 | // Detect hand landmarks 71 | const handResults = this.handLandmarker.detectForVideo(inputElement, Date.now()); 72 | 73 | // Recognize gestures 74 | const gestureResults = this.gestureRecognizer.recognizeForVideo(inputElement, Date.now()); 75 | 76 | this.processResults(handResults, gestureResults); 77 | } 78 | 79 | prepareInputElement(video) { 80 | // Mirror handling code similar to face-detection.mjs 81 | if (this.mirror) { 82 | if (!this.offscreenCanvas) { 83 | this.offscreenCanvas = document.createElement('canvas'); 84 | this.offscreenCtx = this.offscreenCanvas.getContext('2d'); 85 | } 86 | 87 | this.offscreenCanvas.width = video.videoWidth; 88 | this.offscreenCanvas.height = video.videoHeight; 89 | 90 | this.offscreenCtx.save(); 91 | this.offscreenCtx.scale(-1, 1); 92 | this.offscreenCtx.drawImage(video, -video.videoWidth, 0, video.videoWidth, video.videoHeight); 93 | this.offscreenCtx.restore(); 94 | 95 | return this.offscreenCanvas; 96 | } 97 | 98 | return video; 99 | } 100 | 101 | processResults(handResults, gestureResults) { 102 | // Fire events for hand landmarks 103 | if (handResults.landmarks) { 104 | this.app.fire('hands:landmarks', handResults.landmarks); 105 | 106 | // Calculate hand position in 3D space 107 | handResults.landmarks.forEach((landmarks, handIndex) => { 108 | // Use wrist position (landmark 0) as hand position 109 | const wrist = landmarks[0]; 110 | const handPosition = new Vec3( 111 | (wrist.x - 0.5) * 2, // Convert 0-1 to -1 to 1 112 | (0.5 - wrist.y) * 2, // Flip Y and convert to -1 to 1 113 | -wrist.z * 5 // Scale Z for better depth perception 114 | ); 115 | 116 | this.app.fire('hand:position', { handIndex, position: handPosition }); 117 | }); 118 | } 119 | 120 | // Process gestures 121 | if (gestureResults.gestures && gestureResults.gestures.length > 0) { 122 | gestureResults.gestures.forEach((gestureArray, handIndex) => { 123 | if (gestureArray.length > 0) { 124 | // Get the most confident gesture 125 | const { categoryName, score } = gestureArray[0]; 126 | 127 | // Fire event with gesture info 128 | this.app.fire('hand:gesture', handIndex, categoryName, score); 129 | } 130 | }); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /examples/assets/scripts/material-variants.mjs: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', async () => { 2 | await document.querySelector('pc-app').ready(); 3 | 4 | const entityElement = document.querySelector('pc-entity[name="shoe"]'); 5 | const assetElement = document.querySelector('pc-asset#shoe'); 6 | 7 | if (entityElement && assetElement) { 8 | const resource = assetElement.asset.resource; 9 | const variants = resource.getMaterialVariants(); 10 | 11 | // create container for buttons 12 | const buttonContainer = document.createElement('div'); 13 | buttonContainer.classList.add('example-button-container', 'top-right'); 14 | 15 | // create a button for each variant 16 | variants.forEach((variant) => { 17 | const button = document.createElement('button'); 18 | button.textContent = variant.split('-') 19 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 20 | .join(' '); 21 | 22 | button.classList.add('example-button'); 23 | 24 | button.onmouseenter = () => { 25 | button.style.background = 'rgba(255, 255, 255, 1)'; 26 | }; 27 | 28 | button.onmouseleave = () => { 29 | button.style.background = 'rgba(255, 255, 255, 0.9)'; 30 | }; 31 | 32 | button.addEventListener('click', () => { 33 | resource.applyMaterialVariant(entityElement.entity, variant); 34 | }); 35 | 36 | buttonContainer.appendChild(button); 37 | }); 38 | 39 | document.body.appendChild(buttonContainer); 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /examples/assets/scripts/morph-update.mjs: -------------------------------------------------------------------------------- 1 | import { Script } from 'playcanvas'; 2 | 3 | export class MorphUpdate extends Script { 4 | initialize() { 5 | this.app.on('face:blendshapes', (categories) => { 6 | const renders = this.entity.findComponents('render'); 7 | for (const render of renders) { 8 | for (const meshInstance of render.meshInstances) { 9 | if (meshInstance.morphInstance) { 10 | for (const category of categories) { 11 | meshInstance.morphInstance.setWeight(category.categoryName, category.score); 12 | } 13 | } 14 | } 15 | } 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/assets/scripts/orbit.mjs: -------------------------------------------------------------------------------- 1 | import { Script } from 'playcanvas'; 2 | 3 | export class Orbit extends Script { 4 | radius = 4; 5 | 6 | speed = 0.4; 7 | 8 | time = 0; 9 | 10 | update(dt) { 11 | this.time += dt * this.speed; 12 | 13 | const x = Math.cos(this.time) * this.radius; 14 | const z = Math.sin(this.time) * this.radius; 15 | this.entity.setPosition(x, 0, z); 16 | 17 | const dx = -Math.sin(this.time); 18 | const dz = Math.cos(this.time); 19 | 20 | this.entity.lookAt( 21 | x - dx, 22 | 0, 23 | z - dz 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/assets/scripts/rotate.mjs: -------------------------------------------------------------------------------- 1 | import { Script } from 'playcanvas'; 2 | 3 | export class Rotate extends Script { 4 | update(dt) { 5 | this.entity.rotate(10 * dt, 20 * dt, 30 * dt); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/assets/scripts/scroll.mjs: -------------------------------------------------------------------------------- 1 | import { Script } from 'playcanvas'; 2 | 3 | export class Scroll extends Script { 4 | update(dt) { 5 | this.entity.translateLocal(0, dt * 0.5, 0); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/assets/scripts/static-body.mjs: -------------------------------------------------------------------------------- 1 | import { Script } from 'playcanvas'; 2 | 3 | export class StaticBody extends Script { 4 | initialize() { 5 | this.entity.findComponents('render').forEach((render) => { 6 | const entity = render.entity; 7 | entity.addComponent('rigidbody', { 8 | type: 'static' 9 | }); 10 | entity.addComponent('collision', { 11 | type: 'mesh', 12 | renderAsset: render.asset 13 | }); 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/assets/scripts/video-recorder.mjs: -------------------------------------------------------------------------------- 1 | import { Muxer, ArrayBufferTarget } from 'mp4-muxer'; 2 | import { FILLMODE_KEEP_ASPECT, FILLMODE_FILL_WINDOW, RESOLUTION_AUTO, RESOLUTION_FIXED, Script } from 'playcanvas'; 3 | 4 | /** @enum {number} */ 5 | const Resolution = { 6 | SD: 0, // 480p 7 | HD: 1, // 720p 8 | FULLHD: 2 // 1080p 9 | }; 10 | 11 | /** @enum {number} */ 12 | const FrameRate = { 13 | FPS_30: 30, 14 | FPS_60: 60 15 | }; 16 | 17 | export class VideoRecorder extends Script { 18 | /** 19 | * Whether to activate the recorder on initialization. 20 | * 21 | * @attribute 22 | * @type {boolean} 23 | */ 24 | activate = false; 25 | 26 | /** 27 | * The frame rate to record at. 28 | * 29 | * @attribute 30 | * @type {FrameRate} 31 | */ 32 | frameRate = FrameRate.FPS_60; 33 | 34 | /** 35 | * The resolution to record at. 36 | * 37 | * @attribute 38 | * @type {Resolution} 39 | */ 40 | resolution = Resolution.FULLHD; 41 | 42 | /** 43 | * @type {VideoEncoder|null} 44 | * @private 45 | */ 46 | encoder = null; 47 | 48 | /** 49 | * @type {Muxer|null} 50 | * @private 51 | */ 52 | muxer = null; 53 | 54 | /** @private */ 55 | totalFrames = 0; 56 | 57 | /** @private */ 58 | framesEncoded = 0; 59 | 60 | /** @private */ 61 | framesEncodedAtFlush = 0; 62 | 63 | /** @private */ 64 | recording = false; 65 | 66 | /** @private */ 67 | originalUpdate = null; 68 | 69 | initialize() { 70 | if (this.activate) { 71 | this.record(); 72 | } 73 | } 74 | 75 | getVideoSettings() { 76 | switch (this.resolution) { 77 | case Resolution.FULLHD: 78 | return { width: 1920, height: 1080, bitrate: 8_000_000 }; // 8 Mbps for 1080p 79 | case Resolution.HD: 80 | return { width: 1280, height: 720, bitrate: 5_000_000 }; // 5 Mbps for 720p 81 | case Resolution.SD: 82 | default: 83 | return { width: 854, height: 480, bitrate: 2_000_000 }; // 2 Mbps for 480p 84 | } 85 | } 86 | 87 | replaceUpdate() { 88 | // Store the original update function in the instance 89 | this.originalUpdate = this.app.update; 90 | 91 | // Monkey patch with fixed dt based on the requested frame rate 92 | this.app.update = () => this.originalUpdate.call(this.app, 1 / this.frameRate); 93 | } 94 | 95 | restoreUpdate() { 96 | if (this.originalUpdate) { 97 | this.app.update = this.originalUpdate; 98 | this.originalUpdate = null; 99 | } 100 | } 101 | 102 | captureFrame() { 103 | const frame = new VideoFrame(this.app.graphicsDevice.canvas, { 104 | timestamp: this.totalFrames * 1e6 / this.frameRate, 105 | duration: 1e6 / this.frameRate 106 | }); 107 | this.encoder.encode(frame); 108 | frame.close(); 109 | 110 | this.totalFrames++; 111 | } 112 | 113 | /** 114 | * Start recording. 115 | */ 116 | record() { 117 | if (this.recording) return; 118 | this.recording = true; 119 | this.totalFrames = 0; 120 | this.framesEncoded = 0; 121 | this.framesEncodedAtFlush = 0; 122 | 123 | const { width, height, bitrate } = this.getVideoSettings(); 124 | 125 | // Create video frame muxer 126 | this.muxer = new Muxer({ 127 | target: new ArrayBufferTarget(), 128 | video: { 129 | codec: 'avc', 130 | width, 131 | height 132 | }, 133 | fastStart: 'in-memory', 134 | firstTimestampBehavior: 'offset' 135 | }); 136 | 137 | // Create video frame encoder 138 | this.encoder = new VideoEncoder({ 139 | output: (chunk, meta) => { 140 | this.framesEncoded++; 141 | this.muxer.addVideoChunk(chunk, meta); 142 | if (!this.recording) { 143 | this.app.fire('encode:progress', (this.framesEncoded - this.framesEncodedAtFlush) / (this.totalFrames - this.framesEncodedAtFlush)); 144 | } 145 | }, 146 | error: e => console.error(e) 147 | }); 148 | 149 | // Configure encoder with video settings 150 | this.encoder.configure({ 151 | codec: 'avc1.420028', // H.264 codec 152 | width, 153 | height, 154 | bitrate 155 | }); 156 | 157 | // Set canvas to video frame resolution 158 | this.app.setCanvasResolution(RESOLUTION_FIXED, width, height); 159 | this.app.setCanvasFillMode(FILLMODE_KEEP_ASPECT); 160 | 161 | // Start capturing frames 162 | this.app.on('frameend', this.captureFrame, this); 163 | 164 | // Replace update function to fix dt 165 | this.replaceUpdate(); 166 | 167 | console.log('Recording started...'); 168 | } 169 | 170 | /** 171 | * Stop recording. 172 | */ 173 | async stop() { 174 | if (!this.recording) return; 175 | this.recording = false; 176 | this.framesEncodedAtFlush = this.framesEncoded; 177 | 178 | this.app.fire('encode:begin'); 179 | 180 | // Restore update function 181 | this.restoreUpdate(); 182 | 183 | // Disable auto render - this allows CPU/GPU resources to be directed towards encoding 184 | const originalAutoRender = this.app.autoRender; 185 | this.app.autoRender = false; 186 | 187 | // Stop capturing frames 188 | this.app.off('frameend', this.captureFrame, this); 189 | 190 | // Restore canvas fill mode and resolution 191 | this.app.setCanvasResolution(RESOLUTION_AUTO); 192 | this.app.setCanvasFillMode(FILLMODE_FILL_WINDOW); 193 | 194 | // Flush and finalize muxer 195 | await this.encoder.flush(); 196 | this.muxer.finalize(); 197 | 198 | // Download video 199 | const { buffer } = this.muxer.target; 200 | this.app.fire('encode:end', buffer); 201 | 202 | // Free resources 203 | this.encoder.close(); 204 | this.encoder = null; 205 | this.muxer = null; 206 | 207 | // Restore auto render state 208 | this.app.autoRender = originalAutoRender; 209 | 210 | console.log(`Recording stopped. Captured ${this.totalFrames} frames.`); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /examples/assets/scripts/video-texture.mjs: -------------------------------------------------------------------------------- 1 | import { Color, Script, Texture, PIXELFORMAT_R8_G8_B8, FILTER_LINEAR_MIPMAP_LINEAR, FILTER_LINEAR, ADDRESS_CLAMP_TO_EDGE } from 'playcanvas'; 2 | 3 | export class VideoTexture extends Script { 4 | /** 5 | * URL to use if there is no video asset selected. 6 | * @type {string} 7 | * @attribute 8 | */ 9 | url; 10 | 11 | /** 12 | * Material name to apply the video texture to. 13 | * @type {string} 14 | * @attribute 15 | */ 16 | materialName; 17 | 18 | /** 19 | * The video element. 20 | * @type {HTMLVideoElement} 21 | */ 22 | video; 23 | 24 | /** 25 | * The material that the video texture is applied to. 26 | * @type {Material} 27 | */ 28 | material; 29 | 30 | /** 31 | * The handler for the can play through event. 32 | * @type {Function} 33 | */ 34 | _canPlayThroughHandler; 35 | 36 | initialize() { 37 | // Create HTML Video Element to play the video 38 | const video = document.createElement('video'); 39 | video.loop = true; 40 | 41 | // muted attribute is required for videos to autoplay 42 | video.muted = true; 43 | 44 | // critical for iOS or the video won't initially play, and will go fullscreen when playing 45 | video.playsInline = true; 46 | 47 | // needed because the video is being hosted on a different server url 48 | video.crossOrigin = 'anonymous'; 49 | 50 | // autoplay the video 51 | video.autoplay = true; 52 | 53 | // iOS video texture playback requires that you add the video to the DOMParser 54 | // with at least 1x1 as the video's dimensions 55 | const style = video.style; 56 | style.width = '1px'; 57 | style.height = '1px'; 58 | style.position = 'absolute'; 59 | style.opacity = '0'; 60 | style.zIndex = '-1000'; 61 | style.pointerEvents = 'none'; 62 | 63 | document.body.appendChild(video); 64 | 65 | // Create a texture to hold the video frame data 66 | this.videoTexture = new Texture(this.app.graphicsDevice, { 67 | format: PIXELFORMAT_R8_G8_B8, 68 | minFilter: FILTER_LINEAR_MIPMAP_LINEAR, 69 | magFilter: FILTER_LINEAR, 70 | addressU: ADDRESS_CLAMP_TO_EDGE, 71 | addressV: ADDRESS_CLAMP_TO_EDGE, 72 | mipmaps: true 73 | }); 74 | 75 | // Store reference to bound handler 76 | this._canPlayThroughHandler = () => { 77 | const renderComponents = this.entity.findComponents('render'); 78 | renderComponents.forEach((renderComponent) => { 79 | renderComponent.meshInstances.forEach((meshInstance) => { 80 | const material = meshInstance.material; 81 | if (material.name === this.materialName) { 82 | material.emissiveMap = this.videoTexture; 83 | material.emissive = Color.WHITE; 84 | material.update(); 85 | 86 | this.material = material; 87 | } 88 | }); 89 | }); 90 | 91 | this.videoTexture.setSource(video); 92 | video.play(); 93 | }; 94 | 95 | video.addEventListener('canplaythrough', this._canPlayThroughHandler); 96 | 97 | // set video source 98 | video.src = this.url; 99 | video.load(); 100 | 101 | this.video = video; 102 | } 103 | 104 | destroy() { 105 | if (this.material) { 106 | this.material.emissiveMap = null; 107 | this.material.emissive = Color.BLACK; 108 | this.material.update(); 109 | } 110 | 111 | this.videoTexture?.destroy(); 112 | this.videoTexture = null; 113 | 114 | // Stop video playback 115 | if (this.video) { 116 | // Remove event listeners 117 | this.video.removeEventListener('canplaythrough', this._canPlayThroughHandler); 118 | 119 | // Stop loading/playing 120 | this.video.pause(); 121 | 122 | // Clear source and buffer 123 | this.video.removeAttribute('src'); 124 | this.video.load(); // Triggers cleanup of media resources 125 | 126 | // Remove from DOM 127 | this.video.remove(); 128 | 129 | // Clear reference 130 | this.video = null; 131 | } 132 | } 133 | 134 | update() { 135 | // Transfer the latest video frame to the video texture 136 | this.videoTexture?.upload(); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /examples/assets/skies/autumn-field-puresky.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/skies/autumn-field-puresky.webp -------------------------------------------------------------------------------- /examples/assets/skies/dry-lake-bed-2k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/skies/dry-lake-bed-2k.hdr -------------------------------------------------------------------------------- /examples/assets/skies/octagon-lamps-photo-studio-2k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/skies/octagon-lamps-photo-studio-2k.hdr -------------------------------------------------------------------------------- /examples/assets/skies/sepulchral-chapel-rotunda-4k.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/skies/sepulchral-chapel-rotunda-4k.webp -------------------------------------------------------------------------------- /examples/assets/skies/shanghai-riverside-4k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/skies/shanghai-riverside-4k.hdr -------------------------------------------------------------------------------- /examples/assets/skies/stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/skies/stars.png -------------------------------------------------------------------------------- /examples/assets/sounds/clear1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/sounds/clear1.mp3 -------------------------------------------------------------------------------- /examples/assets/sounds/clear4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/sounds/clear4.mp3 -------------------------------------------------------------------------------- /examples/assets/sounds/drop.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/sounds/drop.mp3 -------------------------------------------------------------------------------- /examples/assets/sounds/footsteps.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/sounds/footsteps.mp3 -------------------------------------------------------------------------------- /examples/assets/sounds/gameover.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/sounds/gameover.mp3 -------------------------------------------------------------------------------- /examples/assets/sounds/nyan-cat.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/sounds/nyan-cat.mp3 -------------------------------------------------------------------------------- /examples/assets/sounds/rotate.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/sounds/rotate.mp3 -------------------------------------------------------------------------------- /examples/assets/splats/angel.compressed.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/splats/angel.compressed.ply -------------------------------------------------------------------------------- /examples/assets/splats/biker.compressed.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/splats/biker.compressed.ply -------------------------------------------------------------------------------- /examples/assets/textures/metal-diffuse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/textures/metal-diffuse.jpg -------------------------------------------------------------------------------- /examples/assets/textures/metal-metalness.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/textures/metal-metalness.jpg -------------------------------------------------------------------------------- /examples/assets/textures/metal-normal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/textures/metal-normal.jpg -------------------------------------------------------------------------------- /examples/assets/textures/metal-roughness.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/textures/metal-roughness.jpg -------------------------------------------------------------------------------- /examples/assets/videos/doom.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/assets/videos/doom.mp4 -------------------------------------------------------------------------------- /examples/basic-particles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - Basic Particles 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/basic-shapes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - Basic Shapes 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /examples/car-configurator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - Car Configurator 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /examples/css/browser.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | /* Base layout */ 8 | body { 9 | font-family: system-ui, -apple-system, sans-serif; 10 | display: grid; 11 | grid-template-columns: minmax(250px, max-content) 1fr; 12 | } 13 | 14 | /* Hide menu toggle by default */ 15 | .menu-toggle { 16 | display: none; 17 | color: #2c3e50; 18 | } 19 | 20 | /* Sidebar */ 21 | .sidebar { 22 | background: #f5f5f5; 23 | padding: 20px; 24 | padding-top: max(20px, env(safe-area-inset-top)); 25 | padding-left: max(20px, env(safe-area-inset-left)); 26 | padding-bottom: max(20px, env(safe-area-inset-bottom)); 27 | border-right: 1px solid #ddd; 28 | overflow-y: auto; 29 | min-width: 250px; 30 | width: max-content; 31 | height: 100dvh; 32 | } 33 | 34 | /* Header styles */ 35 | .header { 36 | display: flex; 37 | align-items: center; 38 | gap: 12px; 39 | margin-bottom: 24px; 40 | } 41 | 42 | .logo { 43 | width: 40px; 44 | height: 40px; 45 | } 46 | 47 | .header h2 { 48 | font-size: 18px; 49 | line-height: 1.2; 50 | font-weight: 600; 51 | color: #2c3e50; 52 | } 53 | 54 | /* Example list styles */ 55 | .example-link { 56 | display: flex; 57 | justify-content: space-between; 58 | align-items: center; 59 | padding: 8px 12px; 60 | margin: 4px 0; 61 | border-radius: 4px; 62 | color: #2c3e50; 63 | text-decoration: none; 64 | transition: background-color 0.2s; 65 | } 66 | 67 | .example-link:hover { 68 | background-color: #ffe0cc; 69 | } 70 | 71 | .example-link.active { 72 | background-color: #ff8533; 73 | color: white; 74 | } 75 | 76 | .qr-button, 77 | .open-in-new { 78 | display: flex; 79 | align-items: center; 80 | justify-content: center; 81 | opacity: 0.5; 82 | padding: 4px; 83 | border-radius: 4px; 84 | border: none; 85 | background: none; 86 | cursor: pointer; 87 | color: currentColor; 88 | width: 24px; 89 | height: 24px; 90 | } 91 | 92 | .qr-button:hover, 93 | .open-in-new:hover { 94 | opacity: 1; 95 | background: rgba(0, 0, 0, 0.1); 96 | } 97 | 98 | .qr-button svg, 99 | .open-in-new svg { 100 | stroke-width: 2; 101 | } 102 | 103 | /* Main content */ 104 | .main { 105 | height: 100dvh; 106 | overflow: hidden; 107 | } 108 | 109 | #example-frame { 110 | width: 100%; 111 | height: 100%; 112 | border: none; 113 | } 114 | 115 | /* Mobile styles */ 116 | @media (max-width: 768px) { 117 | body { 118 | display: block; 119 | height: 100vh; 120 | height: 100dvh; 121 | } 122 | 123 | .menu-toggle { 124 | display: flex; 125 | position: fixed; 126 | top: max(16px, env(safe-area-inset-top)); 127 | left: max(16px, env(safe-area-inset-left)); 128 | width: 40px; 129 | height: 40px; 130 | background: rgba(255, 255, 255, 0.9); 131 | border: 1px solid #ddd; 132 | border-radius: 8px; 133 | z-index: 999; 134 | cursor: pointer; 135 | align-items: center; 136 | justify-content: center; 137 | backdrop-filter: blur(8px); 138 | -webkit-backdrop-filter: blur(8px); 139 | } 140 | 141 | .sidebar { 142 | position: fixed; 143 | top: 0; 144 | left: 0; 145 | height: 100%; 146 | width: 280px; 147 | transform: translateX(-100%); 148 | transition: transform 0.3s ease; 149 | z-index: 1000; 150 | } 151 | 152 | .sidebar.open { 153 | transform: translateX(0); 154 | } 155 | 156 | .main { 157 | position: fixed; 158 | inset: 0; 159 | width: 100%; 160 | height: 100%; 161 | } 162 | } 163 | 164 | /* QR Code Modal styles */ 165 | #qr-modal { 166 | position: fixed; 167 | top: 50%; 168 | left: 50%; 169 | transform: translate(-50%, -50%); 170 | padding: 0; 171 | border: none; 172 | border-radius: 8px; 173 | background: white; 174 | margin: 0; 175 | } 176 | 177 | #qr-modal::backdrop { 178 | background: rgba(0, 0, 0, 0.5); 179 | } 180 | 181 | .qr-modal-content { 182 | padding: 24px; 183 | text-align: center; 184 | } 185 | 186 | .qr-modal-content p { 187 | margin-top: 16px; 188 | color: #2c3e50; 189 | } 190 | 191 | #qr-code img { 192 | display: block; 193 | margin: 0 auto; 194 | } 195 | -------------------------------------------------------------------------------- /examples/css/example.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | overflow: hidden; 4 | touch-action: none; 5 | -webkit-touch-callout: none; 6 | -webkit-user-select: none; 7 | user-select: none; 8 | box-sizing: border-box; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | } 13 | 14 | canvas { 15 | touch-action: none; 16 | } 17 | 18 | .example-button-container { 19 | position: absolute; 20 | right: max(16px, env(safe-area-inset-right)); 21 | display: flex; 22 | gap: 8px; 23 | } 24 | 25 | .example-button-container.bottom-right { 26 | bottom: max(16px, env(safe-area-inset-bottom)); 27 | } 28 | 29 | .example-button-container.top-right { 30 | top: max(16px, env(safe-area-inset-top)); 31 | } 32 | 33 | .example-button { 34 | display: flex; 35 | position: relative; 36 | height: 40px; 37 | background: rgba(255, 255, 255, 0.9); 38 | border: 1px solid #ddd; 39 | border-radius: 8px; 40 | cursor: pointer; 41 | align-items: center; 42 | justify-content: center; 43 | padding: 0 16px; 44 | margin: 0; 45 | backdrop-filter: blur(8px); 46 | -webkit-backdrop-filter: blur(8px); 47 | transition: background-color 0.2s; 48 | color: #2c3e50; 49 | font-weight: bold; 50 | } 51 | 52 | .example-button.icon { 53 | width: 40px; 54 | padding: 0; 55 | } 56 | 57 | .example-button:hover { 58 | background: rgba(255, 255, 255, 1); 59 | } 60 | -------------------------------------------------------------------------------- /examples/fps-controller.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - First Person Controller 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/glb.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - GLB Loader 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /examples/img/playcanvas-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/img/playcanvas-192.png -------------------------------------------------------------------------------- /examples/img/playcanvas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/img/playcanvas.png -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components Examples 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 46 |
47 | 48 |
49 | 50 | 55 | 56 | 57 | 58 |
59 |
60 |

Scan to view on mobile

61 |
62 |
63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /examples/js/browser.mjs: -------------------------------------------------------------------------------- 1 | import { examples } from './example-list.mjs'; 2 | import { setupNavigation } from './navigation.mjs'; 3 | import { showQRCode } from './qr-code.mjs'; 4 | 5 | class ExampleBrowser { 6 | constructor() { 7 | this.frame = document.getElementById('example-frame'); 8 | this.exampleList = document.getElementById('example-list'); 9 | this.menuToggle = document.querySelector('.menu-toggle'); 10 | this.sidebar = document.querySelector('.sidebar'); 11 | this.overlay = document.querySelector('.sidebar-overlay'); 12 | 13 | this.setupNavigation(); 14 | this.createExampleList(); 15 | this.loadInitialExample(); 16 | this.setupMobileMenu(); 17 | } 18 | 19 | setupNavigation() { 20 | const loadExample = (path) => { 21 | this.frame.src = path; 22 | }; 23 | this.updateURL = setupNavigation(loadExample).updateURL; 24 | } 25 | 26 | createExampleList() { 27 | examples.forEach((example) => { 28 | const link = this.createExampleLink(example); 29 | this.exampleList.appendChild(link); 30 | }); 31 | } 32 | 33 | createExampleLink(example) { 34 | const link = document.createElement('a'); 35 | link.href = `#${example.path}`; 36 | link.className = 'example-link'; 37 | 38 | const nameSpan = document.createElement('span'); 39 | nameSpan.textContent = example.name; 40 | link.appendChild(nameSpan); 41 | 42 | const buttonContainer = this.createButtonContainer(example); 43 | link.appendChild(buttonContainer); 44 | 45 | link.onclick = e => this.handleExampleClick(e, example, link); 46 | 47 | return link; 48 | } 49 | 50 | createButtonContainer(example) { 51 | const container = document.createElement('div'); 52 | container.style.display = 'flex'; 53 | container.style.gap = '4px'; 54 | 55 | container.appendChild(this.createQRButton(example)); 56 | container.appendChild(this.createOpenInNewButton(example)); 57 | 58 | return container; 59 | } 60 | 61 | createQRButton(example) { 62 | const button = document.createElement('button'); 63 | button.className = 'qr-button'; 64 | button.innerHTML = ` 65 | 66 | 67 | 68 | 69 | 70 | `; 71 | button.title = 'View QR code for mobile'; 72 | button.onclick = (e) => { 73 | e.preventDefault(); 74 | e.stopPropagation(); 75 | showQRCode(example.path); 76 | }; 77 | return button; 78 | } 79 | 80 | createOpenInNewButton(example) { 81 | const button = document.createElement('button'); 82 | button.className = 'open-in-new'; 83 | button.innerHTML = ` 84 | 85 | 86 | 87 | 88 | `; 89 | button.title = 'Open in new tab'; 90 | button.onclick = (e) => { 91 | e.preventDefault(); 92 | e.stopPropagation(); 93 | window.open(example.path, '_blank'); 94 | }; 95 | return button; 96 | } 97 | 98 | handleExampleClick(e, example, link) { 99 | if (e.target.className !== 'open-in-new') { 100 | e.preventDefault(); 101 | this.frame.src = example.path; 102 | this.updateURL(example.path); 103 | document.querySelectorAll('.example-link') 104 | .forEach(l => l.classList.remove('active')); 105 | link.classList.add('active'); 106 | } 107 | } 108 | 109 | loadInitialExample() { 110 | if (examples.length > 0) { 111 | const hash = window.location.hash.slice(1); 112 | if (hash && examples.some(ex => ex.path === hash)) { 113 | this.frame.src = hash; 114 | document.querySelector(`a[href="#${hash}"]`)?.classList.add('active'); 115 | } else { 116 | this.frame.src = examples[0].path; 117 | this.updateURL(examples[0].path); 118 | this.exampleList.firstChild?.classList.add('active'); 119 | } 120 | } 121 | } 122 | 123 | setupMobileMenu() { 124 | const toggleMenu = () => { 125 | this.sidebar.classList.toggle('open'); 126 | this.overlay.classList.toggle('open'); 127 | }; 128 | 129 | this.menuToggle.addEventListener('click', toggleMenu); 130 | this.overlay.addEventListener('click', toggleMenu); 131 | 132 | document.querySelectorAll('.example-link').forEach((link) => { 133 | link.addEventListener('click', () => { 134 | if (window.innerWidth <= 768) { 135 | toggleMenu(); 136 | } 137 | }); 138 | }); 139 | } 140 | } 141 | 142 | // Initialize the browser when the DOM is ready 143 | document.addEventListener('DOMContentLoaded', () => { 144 | const browser = new ExampleBrowser(); /* eslint-disable-line no-unused-vars */ 145 | }); 146 | -------------------------------------------------------------------------------- /examples/js/example-list.mjs: -------------------------------------------------------------------------------- 1 | export const examples = [ 2 | { name: 'Animation', path: 'animation.html' }, 3 | { name: 'Annotations', path: 'annotations.html' }, 4 | { name: 'AR Avatar', path: 'ar-avatar.html' }, 5 | { name: 'Basic Shapes', path: 'basic-shapes.html' }, 6 | { name: 'Basic Particles', path: 'basic-particles.html' }, 7 | { name: 'Car Configurator', path: 'car-configurator.html' }, 8 | { name: 'FPS Controller', path: 'fps-controller.html' }, 9 | { name: 'Gaussian Splatting', path: 'splat.html' }, 10 | { name: 'GLB Loader', path: 'glb.html' }, 11 | { name: 'Physics', path: 'physics.html' }, 12 | { name: 'Physics Cluster', path: 'physics-cluster.html' }, 13 | { name: 'Positional Sound', path: 'positional-sound.html' }, 14 | { name: 'Shoe Configurator', path: 'shoe-configurator.html' }, 15 | { name: 'Solar System', path: 'solar-system.html' }, 16 | { name: 'Sound', path: 'sound.html' }, 17 | { name: 'Text Elements', path: 'text.html' }, 18 | { name: 'Text 3D', path: 'text3d.html' }, 19 | { name: 'Tweening', path: 'tween.html' }, 20 | { name: 'Vibe Falling Blocks', path: 'vibe-falling-blocks.html' }, 21 | { name: 'Video Recorder', path: 'video-recorder.html' }, 22 | { name: 'Video Texture', path: 'video-texture.html' } 23 | ]; 24 | -------------------------------------------------------------------------------- /examples/js/navigation.mjs: -------------------------------------------------------------------------------- 1 | import { examples } from './example-list.mjs'; 2 | 3 | export function setupNavigation(loadExample) { 4 | function updateURL(path) { 5 | history.pushState(null, '', `#${path}`); 6 | } 7 | 8 | window.addEventListener('popstate', () => { 9 | const hash = window.location.hash.slice(1); 10 | if (hash && examples.some(ex => ex.path === hash)) { 11 | loadExample(hash); 12 | document.querySelectorAll('.example-link').forEach((link) => { 13 | link.classList.toggle('active', link.getAttribute('href') === `#${hash}`); 14 | }); 15 | } 16 | }); 17 | 18 | document.addEventListener('keydown', (e) => { 19 | if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { 20 | e.preventDefault(); 21 | const links = Array.from(document.querySelectorAll('.example-link')); 22 | const currentIndex = links.findIndex(link => link.classList.contains('active')); 23 | const nextIndex = e.key === 'ArrowUp' ? 24 | Math.max(0, currentIndex - 1) : 25 | Math.min(links.length - 1, currentIndex + 1); 26 | 27 | links[nextIndex].click(); 28 | } 29 | }); 30 | 31 | return { updateURL }; 32 | } 33 | -------------------------------------------------------------------------------- /examples/js/qr-code.mjs: -------------------------------------------------------------------------------- 1 | export function showQRCode(path) { 2 | const qr = window.qrcode(0, 'L'); 3 | const url = `${window.location.origin}${window.location.pathname}${path}`; 4 | qr.addData(url); 5 | qr.make(); 6 | 7 | const modal = document.getElementById('qr-modal'); 8 | const qrDiv = document.getElementById('qr-code'); 9 | qrDiv.innerHTML = qr.createImgTag(4); 10 | 11 | // Add click handler to close on backdrop click 12 | modal.addEventListener('click', (e) => { 13 | const rect = modal.getBoundingClientRect(); 14 | const isInDialog = (rect.top <= e.clientY && e.clientY <= rect.top + rect.height && 15 | rect.left <= e.clientX && e.clientX <= rect.left + rect.width); 16 | if (!isInDialog) { 17 | modal.close(); 18 | } 19 | }); 20 | 21 | modal.showModal(); 22 | } 23 | -------------------------------------------------------------------------------- /examples/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PlayCanvas Web Components Examples", 3 | "short_name": "PlayCanvas Examples", 4 | "description": "Examples showcasing PlayCanvas Web Components capabilities and features", 5 | "start_url": "./index.html", 6 | "display": "standalone", 7 | "background_color": "#ffffff", 8 | "theme_color": "#ffffff", 9 | "icons": [ 10 | { 11 | "src": "img/playcanvas-192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "img/playcanvas.png", 17 | "sizes": "512x512", 18 | "type": "image/png" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /examples/modules/ammo/ammo.wasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/modules/ammo/ammo.wasm.wasm -------------------------------------------------------------------------------- /examples/modules/basis/basis.wasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/modules/basis/basis.wasm.wasm -------------------------------------------------------------------------------- /examples/modules/draco/draco.wasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/examples/modules/draco/draco.wasm.wasm -------------------------------------------------------------------------------- /examples/physics-cluster.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - Physics Cluster 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 47 | 48 | 49 | 50 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /examples/physics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - Physics 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /examples/positional-sound.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - Positional Sound 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /examples/screen.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - 2D Screen 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/shoe-configurator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - GLB Loader 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /examples/sound.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - GLB Loader 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /examples/spinning-cube-api.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - Spinning Cube via DOM API 7 | 14 | 15 | 16 | 17 | 18 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /examples/spinning-cube-umd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - Spinning Cube 7 | 8 | 9 | 10 | 11 | 12 | 13 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/spinning-cube.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - Spinning Cube 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/splat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - 3D Gaussian Splat 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /examples/text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - Text 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /examples/text3d.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - 3D Text 7 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /examples/tween.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - Tweening 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 38 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /examples/video-recorder.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - Video Encoder 7 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 49 | 50 | 51 | 52 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /examples/video-texture.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PlayCanvas Web Components - Video Textures 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 67 | 68 | 69 | 70 | 71 | 72 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /lib/meshopt_decoder.module.d.ts: -------------------------------------------------------------------------------- 1 | // This file is part of meshoptimizer library and is distributed under the terms of MIT License. 2 | // Copyright (C) 2016-2022, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) 3 | export const MeshoptDecoder: { 4 | supported: boolean; 5 | ready: Promise; 6 | 7 | decodeVertexBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array, filter?: string) => void; 8 | decodeIndexBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array) => void; 9 | decodeIndexSequence: (target: Uint8Array, count: number, size: number, source: Uint8Array) => void; 10 | 11 | decodeGltfBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array, mode: string, filter?: string) => void; 12 | 13 | useWorkers: (count: number) => void; 14 | decodeGltfBufferAsync: (count: number, size: number, source: Uint8Array, mode: string, filter?: string) => Promise; 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@playcanvas/web-components", 3 | "version": "0.2.6", 4 | "author": "PlayCanvas ", 5 | "homepage": "https://playcanvas.com", 6 | "description": "Web Components for the PlayCanvas Engine", 7 | "keywords": [ 8 | "custom-elements", 9 | "declarative", 10 | "html", 11 | "playcanvas", 12 | "typescript", 13 | "web-components", 14 | "webgl", 15 | "webgpu", 16 | "webxr" 17 | ], 18 | "license": "MIT", 19 | "main": "dist/pwc.cjs", 20 | "module": "dist/pwc.mjs", 21 | "browser": "dist/pwc.js", 22 | "types": "dist/index.d.ts", 23 | "type": "module", 24 | "files": [ 25 | "dist", 26 | "src" 27 | ], 28 | "scripts": { 29 | "build": "rollup -c", 30 | "dev": "concurrently \"npm run watch\" \"npm run serve\"", 31 | "docs": "typedoc", 32 | "lint": "eslint examples/js examples/assets/scripts src", 33 | "serve": "serve", 34 | "test": "echo \"Error: no test specified\" && exit 1", 35 | "type-check": "tsc --noEmit", 36 | "type-check:watch": "npm run type-check -- --watch", 37 | "watch": "rollup -c -w" 38 | }, 39 | "devDependencies": { 40 | "@mediapipe/tasks-vision": "0.10.21", 41 | "@playcanvas/eslint-config": "2.1.0", 42 | "@rollup/plugin-commonjs": "28.0.3", 43 | "@rollup/plugin-node-resolve": "16.0.1", 44 | "@rollup/plugin-terser": "0.4.4", 45 | "@rollup/plugin-typescript": "12.1.2", 46 | "@tweenjs/tween.js": "25.0.0", 47 | "@typescript-eslint/eslint-plugin": "8.33.0", 48 | "@typescript-eslint/parser": "8.33.0", 49 | "concurrently": "9.1.2", 50 | "earcut": "3.0.1", 51 | "eslint": "9.28.0", 52 | "eslint-import-resolver-typescript": "4.4.2", 53 | "globals": "16.2.0", 54 | "mp4-muxer": "5.2.1", 55 | "opentype.js": "1.3.4", 56 | "playcanvas": "2.7.7", 57 | "rollup": "4.41.1", 58 | "serve": "14.2.4", 59 | "tslib": "2.8.1", 60 | "typedoc": "0.28.5", 61 | "typedoc-plugin-mdn-links": "5.0.2", 62 | "typescript": "5.8.3" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import terser from '@rollup/plugin-terser'; 4 | import typescript from '@rollup/plugin-typescript'; 5 | 6 | export default { 7 | input: 'src/index.ts', 8 | output: [ 9 | { 10 | file: 'dist/pwc.mjs', 11 | format: 'esm', 12 | sourcemap: true 13 | }, 14 | { 15 | file: 'dist/pwc.cjs', 16 | format: 'cjs', 17 | sourcemap: true 18 | }, 19 | { 20 | file: 'dist/pwc.js', 21 | name: 'pd', 22 | format: 'umd', 23 | sourcemap: true, 24 | globals: { playcanvas: 'pc' } 25 | }, 26 | { 27 | file: 'dist/pwc.min.js', 28 | name: 'pd', 29 | format: 'umd', 30 | sourcemap: true, 31 | plugins: [terser()], 32 | globals: { playcanvas: 'pc' } 33 | } 34 | ], 35 | plugins: [ 36 | resolve(), 37 | commonjs(), 38 | typescript({ 39 | tsconfig: './tsconfig.json', // Path to your tsconfig.json 40 | declaration: true, 41 | declarationDir: './dist', 42 | sourceMap: true 43 | }) 44 | ], 45 | external: ['playcanvas'] 46 | }; 47 | -------------------------------------------------------------------------------- /src/asset.ts: -------------------------------------------------------------------------------- 1 | import { Asset } from 'playcanvas'; 2 | 3 | import { MeshoptDecoder } from '../lib/meshopt_decoder.module.js'; 4 | 5 | const extToType = new Map([ 6 | ['bin', 'binary'], 7 | ['css', 'css'], 8 | ['frag', 'shader'], 9 | ['glb', 'container'], 10 | ['glsl', 'shader'], 11 | ['hdr', 'texture'], 12 | ['html', 'html'], 13 | ['jpg', 'texture'], 14 | ['js', 'script'], 15 | ['json', 'json'], 16 | ['mp3', 'audio'], 17 | ['mjs', 'script'], 18 | ['ply', 'gsplat'], 19 | ['png', 'texture'], 20 | ['txt', 'text'], 21 | ['vert', 'shader'], 22 | ['webp', 'texture'] 23 | ]); 24 | 25 | 26 | // provide buffer view callback so we can handle models compressed with MeshOptimizer 27 | // https://github.com/zeux/meshoptimizer 28 | const processBufferView = ( 29 | gltfBuffer: any, 30 | buffers: Array, 31 | continuation: (err: string | null, result: any) => void 32 | ) => { 33 | if (gltfBuffer.extensions && gltfBuffer.extensions.EXT_meshopt_compression) { 34 | const extensionDef = gltfBuffer.extensions.EXT_meshopt_compression; 35 | 36 | Promise.all([MeshoptDecoder.ready, buffers[extensionDef.buffer]]).then((promiseResult) => { 37 | const buffer = promiseResult[1]; 38 | 39 | const byteOffset = extensionDef.byteOffset || 0; 40 | const byteLength = extensionDef.byteLength || 0; 41 | 42 | const count = extensionDef.count; 43 | const stride = extensionDef.byteStride; 44 | 45 | const result = new Uint8Array(count * stride); 46 | const source = new Uint8Array(buffer.buffer, buffer.byteOffset + byteOffset, byteLength); 47 | 48 | MeshoptDecoder.decodeGltfBuffer( 49 | result, 50 | count, 51 | stride, 52 | source, 53 | extensionDef.mode, 54 | extensionDef.filter 55 | ); 56 | 57 | continuation(null, result); 58 | }); 59 | } else { 60 | continuation(null, null); 61 | } 62 | }; 63 | 64 | /** 65 | * The AssetElement interface provides properties and methods for manipulating 66 | * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-asset/ | ``} elements. 67 | * The AssetElement interface also inherits the properties and methods of the 68 | * {@link HTMLElement} interface. 69 | */ 70 | class AssetElement extends HTMLElement { 71 | private _lazy: boolean = false; 72 | 73 | /** 74 | * The asset that is loaded. 75 | */ 76 | asset: Asset | null = null; 77 | 78 | disconnectedCallback() { 79 | this.destroyAsset(); 80 | } 81 | 82 | createAsset() { 83 | const id = this.getAttribute('id') || ''; 84 | const src = this.getAttribute('src') || ''; 85 | let type = this.getAttribute('type'); 86 | 87 | // If no type is specified, try to infer it from the file extension. 88 | if (!type) { 89 | const ext = src.split('.').pop(); 90 | type = extToType.get(ext || '') ?? null; 91 | } 92 | 93 | if (!type) { 94 | console.warn(`Unsupported asset type: ${src}`); 95 | return; 96 | } 97 | 98 | if (type === 'container') { 99 | this.asset = new Asset(id, type, { url: src }, undefined, { 100 | // @ts-ignore TODO no definition in pc 101 | bufferView: { 102 | processAsync: processBufferView.bind(this) 103 | } 104 | }); 105 | } else { 106 | // @ts-ignore 107 | this.asset = new Asset(id, type, { url: src }); 108 | } 109 | 110 | this.asset.preload = !this._lazy; 111 | } 112 | 113 | 114 | destroyAsset() { 115 | if (this.asset) { 116 | this.asset.unload(); 117 | this.asset = null; 118 | } 119 | } 120 | 121 | /** 122 | * Sets whether the asset should be loaded lazily. 123 | * @param value - The lazy loading flag. 124 | */ 125 | set lazy(value: boolean) { 126 | this._lazy = value; 127 | if (this.asset) { 128 | this.asset.preload = !value; 129 | } 130 | } 131 | 132 | /** 133 | * Gets whether the asset should be loaded lazily. 134 | * @returns The lazy loading flag. 135 | */ 136 | get lazy() { 137 | return this._lazy; 138 | } 139 | 140 | static get(id: string) { 141 | const assetElement = document.querySelector(`pc-asset[id="${id}"]`); 142 | return assetElement?.asset; 143 | } 144 | 145 | static get observedAttributes() { 146 | return ['lazy']; 147 | } 148 | 149 | attributeChangedCallback(name: string, _oldValue: string, _newValue: string) { 150 | if (name === 'lazy') { 151 | this.lazy = this.hasAttribute('lazy'); 152 | } 153 | } 154 | } 155 | 156 | customElements.define('pc-asset', AssetElement); 157 | 158 | export { AssetElement }; 159 | -------------------------------------------------------------------------------- /src/async-element.ts: -------------------------------------------------------------------------------- 1 | import { AppElement } from './app'; 2 | import { EntityElement } from './entity'; 3 | 4 | /** 5 | * Base class for all PlayCanvas Web Components that initialize asynchronously. 6 | */ 7 | class AsyncElement extends HTMLElement { 8 | private _readyPromise: Promise; 9 | 10 | private _readyResolve!: () => void; 11 | 12 | /** @ignore */ 13 | constructor() { 14 | super(); 15 | this._readyPromise = new Promise((resolve) => { 16 | this._readyResolve = resolve; 17 | }); 18 | } 19 | 20 | get closestApp(): AppElement { 21 | return this.parentElement?.closest('pc-app') as AppElement; 22 | } 23 | 24 | get closestEntity(): EntityElement { 25 | return this.parentElement?.closest('pc-entity') as EntityElement; 26 | } 27 | 28 | /** 29 | * Called when the element is fully initialized and ready. 30 | * Subclasses should call this when they're ready. 31 | */ 32 | protected _onReady() { 33 | this._readyResolve(); 34 | this.dispatchEvent(new CustomEvent('ready')); 35 | } 36 | 37 | /** 38 | * Returns a promise that resolves with this element when it's ready. 39 | * @returns A promise that resolves with this element when it's ready. 40 | */ 41 | ready(): Promise { 42 | return this._readyPromise.then(() => this); 43 | } 44 | } 45 | 46 | export { AsyncElement }; 47 | -------------------------------------------------------------------------------- /src/colors.ts: -------------------------------------------------------------------------------- 1 | export const CSS_COLORS: Record = { 2 | aliceblue: '#f0f8ff', 3 | antiquewhite: '#faebd7', 4 | aqua: '#00ffff', 5 | aquamarine: '#7fffd4', 6 | azure: '#f0ffff', 7 | beige: '#f5f5dc', 8 | bisque: '#ffe4c4', 9 | black: '#000000', 10 | blanchedalmond: '#ffebcd', 11 | blue: '#0000ff', 12 | blueviolet: '#8a2be2', 13 | brown: '#a52a2a', 14 | burlywood: '#deb887', 15 | cadetblue: '#5f9ea0', 16 | chartreuse: '#7fff00', 17 | chocolate: '#d2691e', 18 | coral: '#ff7f50', 19 | cornflowerblue: '#6495ed', 20 | cornsilk: '#fff8dc', 21 | crimson: '#dc143c', 22 | cyan: '#00ffff', 23 | darkblue: '#00008b', 24 | darkcyan: '#008b8b', 25 | darkgoldenrod: '#b8860b', 26 | darkgray: '#a9a9a9', 27 | darkgreen: '#006400', 28 | darkgrey: '#a9a9a9', 29 | darkkhaki: '#bdb76b', 30 | darkmagenta: '#8b008b', 31 | darkolivegreen: '#556b2f', 32 | darkorange: '#ff8c00', 33 | darkorchid: '#9932cc', 34 | darkred: '#8b0000', 35 | darksalmon: '#e9967a', 36 | darkseagreen: '#8fbc8f', 37 | darkslateblue: '#483d8b', 38 | darkslategray: '#2f4f4f', 39 | darkslategrey: '#2f4f4f', 40 | darkturquoise: '#00ced1', 41 | darkviolet: '#9400d3', 42 | deeppink: '#ff1493', 43 | deepskyblue: '#00bfff', 44 | dimgray: '#696969', 45 | dimgrey: '#696969', 46 | dodgerblue: '#1e90ff', 47 | firebrick: '#b22222', 48 | floralwhite: '#fffaf0', 49 | forestgreen: '#228b22', 50 | fuchsia: '#ff00ff', 51 | gainsboro: '#dcdcdc', 52 | ghostwhite: '#f8f8ff', 53 | gold: '#ffd700', 54 | goldenrod: '#daa520', 55 | gray: '#808080', 56 | green: '#008000', 57 | greenyellow: '#adff2f', 58 | grey: '#808080', 59 | honeydew: '#f0fff0', 60 | hotpink: '#ff69b4', 61 | indianred: '#cd5c5c', 62 | indigo: '#4b0082', 63 | ivory: '#fffff0', 64 | khaki: '#f0e68c', 65 | lavender: '#e6e6fa', 66 | lavenderblush: '#fff0f5', 67 | lawngreen: '#7cfc00', 68 | lemonchiffon: '#fffacd', 69 | lightblue: '#add8e6', 70 | lightcoral: '#f08080', 71 | lightcyan: '#e0ffff', 72 | lightgoldenrodyellow: '#fafad2', 73 | lightgray: '#d3d3d3', 74 | lightgreen: '#90ee90', 75 | lightgrey: '#d3d3d3', 76 | lightpink: '#ffb6c1', 77 | lightsalmon: '#ffa07a', 78 | lightseagreen: '#20b2aa', 79 | lightskyblue: '#87cefa', 80 | lightslategray: '#778899', 81 | lightslategrey: '#778899', 82 | lightsteelblue: '#b0c4de', 83 | lightyellow: '#ffffe0', 84 | lime: '#00ff00', 85 | limegreen: '#32cd32', 86 | linen: '#faf0e6', 87 | magenta: '#ff00ff', 88 | maroon: '#800000', 89 | mediumaquamarine: '#66cdaa', 90 | mediumblue: '#0000cd', 91 | mediumorchid: '#ba55d3', 92 | mediumpurple: '#9370db', 93 | mediumseagreen: '#3cb371', 94 | mediumslateblue: '#7b68ee', 95 | mediumspringgreen: '#00fa9a', 96 | mediumturquoise: '#48d1cc', 97 | mediumvioletred: '#c71585', 98 | midnightblue: '#191970', 99 | mintcream: '#f5fffa', 100 | mistyrose: '#ffe4e1', 101 | moccasin: '#ffe4b5', 102 | navajowhite: '#ffdead', 103 | navy: '#000080', 104 | oldlace: '#fdf5e6', 105 | olive: '#808000', 106 | olivedrab: '#6b8e23', 107 | orange: '#ffa500', 108 | orangered: '#ff4500', 109 | orchid: '#da70d6', 110 | palegoldenrod: '#eee8aa', 111 | palegreen: '#98fb98', 112 | paleturquoise: '#afeeee', 113 | palevioletred: '#db7093', 114 | papayawhip: '#ffefd5', 115 | peachpuff: '#ffdab9', 116 | peru: '#cd853f', 117 | pink: '#ffc0cb', 118 | plum: '#dda0dd', 119 | powderblue: '#b0e0e6', 120 | purple: '#800080', 121 | rebeccapurple: '#663399', 122 | red: '#ff0000', 123 | rosybrown: '#bc8f8f', 124 | royalblue: '#4169e1', 125 | saddlebrown: '#8b4513', 126 | salmon: '#fa8072', 127 | sandybrown: '#f4a460', 128 | seagreen: '#2e8b57', 129 | seashell: '#fff5ee', 130 | sienna: '#a0522d', 131 | silver: '#c0c0c0', 132 | skyblue: '#87ceeb', 133 | slateblue: '#6a5acd', 134 | slategray: '#708090', 135 | slategrey: '#708090', 136 | snow: '#fffafa', 137 | springgreen: '#00ff7f', 138 | steelblue: '#4682b4', 139 | tan: '#d2b48c', 140 | teal: '#008080', 141 | thistle: '#d8bfd8', 142 | tomato: '#ff6347', 143 | turquoise: '#40e0d0', 144 | violet: '#ee82ee', 145 | wheat: '#f5deb3', 146 | white: '#ffffff', 147 | whitesmoke: '#f5f5f5', 148 | yellow: '#ffff00', 149 | yellowgreen: '#9acd32' 150 | }; 151 | -------------------------------------------------------------------------------- /src/components/collision-component.ts: -------------------------------------------------------------------------------- 1 | import { CollisionComponent, Quat, Vec3 } from 'playcanvas'; 2 | 3 | import { ComponentElement } from './component'; 4 | import { parseQuat, parseVec3 } from '../utils'; 5 | 6 | /** 7 | * The CollisionComponentElement interface provides properties and methods for manipulating 8 | * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-collision/ | ``} elements. 9 | * The CollisionComponentElement interface also inherits the properties and methods of the 10 | * {@link HTMLElement} interface. 11 | * 12 | * @category Components 13 | */ 14 | class CollisionComponentElement extends ComponentElement { 15 | private _angularOffset: Quat = new Quat(); 16 | 17 | private _axis: number = 1; 18 | 19 | private _convexHull: boolean = false; 20 | 21 | private _halfExtents: Vec3 = new Vec3(0.5, 0.5, 0.5); 22 | 23 | private _height: number = 2; 24 | 25 | private _linearOffset: Vec3 = new Vec3(); 26 | 27 | private _radius: number = 0.5; 28 | 29 | private _type: string = 'box'; 30 | 31 | /** @ignore */ 32 | constructor() { 33 | super('collision'); 34 | } 35 | 36 | getInitialComponentData() { 37 | return { 38 | axis: this._axis, 39 | angularOffset: this._angularOffset, 40 | convexHull: this._convexHull, 41 | halfExtents: this._halfExtents, 42 | height: this._height, 43 | linearOffset: this._linearOffset, 44 | radius: this._radius, 45 | type: this._type 46 | }; 47 | } 48 | 49 | /** 50 | * Gets the underlying PlayCanvas collision component. 51 | * @returns The collision component. 52 | */ 53 | get component(): CollisionComponent | null { 54 | return super.component as CollisionComponent | null; 55 | } 56 | 57 | set angularOffset(value: Quat) { 58 | this._angularOffset = value; 59 | if (this.component) { 60 | this.component.angularOffset = value; 61 | } 62 | } 63 | 64 | get angularOffset() { 65 | return this._angularOffset; 66 | } 67 | 68 | set axis(value: number) { 69 | this._axis = value; 70 | if (this.component) { 71 | this.component.axis = value; 72 | } 73 | } 74 | 75 | get axis() { 76 | return this._axis; 77 | } 78 | 79 | set convexHull(value: boolean) { 80 | this._convexHull = value; 81 | if (this.component) { 82 | this.component.convexHull = value; 83 | } 84 | } 85 | 86 | get convexHull() { 87 | return this._convexHull; 88 | } 89 | 90 | set halfExtents(value: Vec3) { 91 | this._halfExtents = value; 92 | if (this.component) { 93 | this.component.halfExtents = value; 94 | } 95 | } 96 | 97 | get halfExtents() { 98 | return this._halfExtents; 99 | } 100 | 101 | set height(value: number) { 102 | this._height = value; 103 | if (this.component) { 104 | this.component.height = value; 105 | } 106 | } 107 | 108 | get height() { 109 | return this._height; 110 | } 111 | 112 | set linearOffset(value: Vec3) { 113 | this._linearOffset = value; 114 | if (this.component) { 115 | this.component.linearOffset = value; 116 | } 117 | } 118 | 119 | get linearOffset() { 120 | return this._linearOffset; 121 | } 122 | 123 | set radius(value: number) { 124 | this._radius = value; 125 | if (this.component) { 126 | this.component.radius = value; 127 | } 128 | } 129 | 130 | get radius() { 131 | return this._radius; 132 | } 133 | 134 | set type(value: string) { 135 | this._type = value; 136 | if (this.component) { 137 | this.component.type = value; 138 | } 139 | } 140 | 141 | get type() { 142 | return this._type; 143 | } 144 | 145 | static get observedAttributes() { 146 | return [...super.observedAttributes, 'angular-offset', 'axis', 'convex-hull', 'half-extents', 'height', 'linear-offset', 'radius', 'type']; 147 | } 148 | 149 | attributeChangedCallback(name: string, _oldValue: string, newValue: string) { 150 | super.attributeChangedCallback(name, _oldValue, newValue); 151 | 152 | switch (name) { 153 | case 'angular-offset': 154 | this.angularOffset = parseQuat(newValue); 155 | break; 156 | case 'axis': 157 | this.axis = parseInt(newValue, 10); 158 | break; 159 | case 'convex-hull': 160 | this.convexHull = this.hasAttribute('convex-hull'); 161 | break; 162 | case 'half-extents': 163 | this.halfExtents = parseVec3(newValue); 164 | break; 165 | case 'height': 166 | this.height = parseFloat(newValue); 167 | break; 168 | case 'linear-offset': 169 | this.linearOffset = parseVec3(newValue); 170 | break; 171 | case 'radius': 172 | this.radius = parseFloat(newValue); 173 | break; 174 | case 'type': 175 | this.type = newValue; 176 | break; 177 | } 178 | } 179 | } 180 | 181 | customElements.define('pc-collision', CollisionComponentElement); 182 | 183 | export { CollisionComponentElement }; 184 | -------------------------------------------------------------------------------- /src/components/component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from 'playcanvas'; 2 | 3 | import { AsyncElement } from '../async-element'; 4 | 5 | /** 6 | * Represents a component in the PlayCanvas engine. 7 | * 8 | * @category Components 9 | */ 10 | class ComponentElement extends AsyncElement { 11 | private _componentName: string; 12 | 13 | private _enabled = true; 14 | 15 | private _component: Component | null = null; 16 | 17 | /** 18 | * Creates a new ComponentElement instance. 19 | * 20 | * @param componentName - The name of the component. 21 | * @ignore 22 | */ 23 | constructor(componentName: string) { 24 | super(); 25 | 26 | this._componentName = componentName; 27 | } 28 | 29 | // Method to be overridden by subclasses to provide initial component data 30 | getInitialComponentData() { 31 | return {}; 32 | } 33 | 34 | async addComponent() { 35 | const entityElement = this.closestEntity; 36 | if (entityElement) { 37 | await entityElement.ready(); 38 | // Add the component to the entity 39 | const data = this.getInitialComponentData(); 40 | this._component = entityElement.entity!.addComponent(this._componentName, data); 41 | } 42 | } 43 | 44 | initComponent() {} 45 | 46 | async connectedCallback() { 47 | await this.closestApp?.ready(); 48 | await this.addComponent(); 49 | this.initComponent(); 50 | this._onReady(); 51 | } 52 | 53 | disconnectedCallback() { 54 | // Remove the component when the element is disconnected 55 | if (this.component && this.component.entity) { 56 | this._component!.entity.removeComponent(this._componentName); 57 | this._component = null; 58 | } 59 | } 60 | 61 | get component(): Component | null { 62 | return this._component; 63 | } 64 | 65 | /** 66 | * Sets the enabled state of the component. 67 | * @param value - The enabled state of the component. 68 | */ 69 | set enabled(value: boolean) { 70 | this._enabled = value; 71 | if (this.component) { 72 | this.component.enabled = value; 73 | } 74 | } 75 | 76 | /** 77 | * Gets the enabled state of the component. 78 | * @returns The enabled state of the component. 79 | */ 80 | get enabled() { 81 | return this._enabled; 82 | } 83 | 84 | static get observedAttributes() { 85 | return ['enabled']; 86 | } 87 | 88 | attributeChangedCallback(name: string, _oldValue: string, newValue: string) { 89 | switch (name) { 90 | case 'enabled': 91 | this.enabled = newValue !== 'false'; 92 | break; 93 | } 94 | } 95 | } 96 | 97 | export { ComponentElement }; 98 | -------------------------------------------------------------------------------- /src/components/listener-component.ts: -------------------------------------------------------------------------------- 1 | import { AudioListenerComponent } from 'playcanvas'; 2 | 3 | import { ComponentElement } from './component'; 4 | 5 | /** 6 | * The ListenerComponentElement interface provides properties and methods for manipulating 7 | * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-listener/ | ``} elements. 8 | * The ListenerComponentElement interface also inherits the properties and methods of the 9 | * {@link HTMLElement} interface. 10 | * 11 | * @category Components 12 | */ 13 | class ListenerComponentElement extends ComponentElement { 14 | /** @ignore */ 15 | constructor() { 16 | super('audiolistener'); 17 | } 18 | 19 | /** 20 | * Gets the underlying PlayCanvas audio listener component. 21 | * @returns The audio listener component. 22 | */ 23 | get component(): AudioListenerComponent | null { 24 | return super.component as AudioListenerComponent | null; 25 | } 26 | } 27 | 28 | customElements.define('pc-listener', ListenerComponentElement); 29 | 30 | export { ListenerComponentElement }; 31 | -------------------------------------------------------------------------------- /src/components/particlesystem-component.ts: -------------------------------------------------------------------------------- 1 | import { ParticleSystemComponent } from 'playcanvas'; 2 | 3 | import { ComponentElement } from './component'; 4 | import { AssetElement } from '../asset'; 5 | 6 | /** 7 | * The ParticleSystemComponentElement interface provides properties and methods for manipulating 8 | * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-particles/ | ``} elements. 9 | * The ParticleSystemComponentElement interface also inherits the properties and methods of the 10 | * {@link HTMLElement} interface. 11 | * 12 | * @category Components 13 | */ 14 | class ParticleSystemComponentElement extends ComponentElement { 15 | private _asset: string = ''; 16 | 17 | /** @ignore */ 18 | constructor() { 19 | super('particlesystem'); 20 | } 21 | 22 | getInitialComponentData() { 23 | const asset = AssetElement.get(this._asset); 24 | if (!asset) { 25 | return {}; 26 | } 27 | 28 | if ((asset.resource as any).colorMapAsset) { 29 | const id = (asset.resource as any).colorMapAsset; 30 | const colorMapAsset = AssetElement.get(id)?.id; 31 | if (colorMapAsset) { 32 | (asset.resource as any).colorMapAsset = colorMapAsset; 33 | } 34 | } 35 | 36 | return asset.resource; 37 | } 38 | 39 | /** 40 | * Gets the underlying PlayCanvas particle system component. 41 | * @returns The particle system component. 42 | */ 43 | get component(): ParticleSystemComponent | null { 44 | return super.component as ParticleSystemComponent | null; 45 | } 46 | 47 | private applyConfig(resource: any) { 48 | if (!this.component) { 49 | return; 50 | } 51 | 52 | // Set all the config properties on the component 53 | for (const key in resource) { 54 | if (resource.hasOwnProperty(key)) { 55 | (this.component as any)[key] = resource[key]; 56 | } 57 | } 58 | } 59 | 60 | private async _loadAsset() { 61 | const appElement = await this.closestApp?.ready(); 62 | const app = appElement?.app; 63 | 64 | const asset = AssetElement.get(this._asset); 65 | if (!asset) { 66 | return; 67 | } 68 | 69 | if (asset.loaded) { 70 | this.applyConfig(asset.resource); 71 | } else { 72 | asset.once('load', () => { 73 | this.applyConfig(asset.resource); 74 | }); 75 | app!.assets.load(asset); 76 | } 77 | } 78 | 79 | /** 80 | * Sets the id of the `pc-asset` to use for the model. 81 | * @param value - The asset ID. 82 | */ 83 | set asset(value: string) { 84 | this._asset = value; 85 | if (this.isConnected) { 86 | this._loadAsset(); 87 | } 88 | } 89 | 90 | /** 91 | * Gets the id of the `pc-asset` to use for the model. 92 | * @returns The asset ID. 93 | */ 94 | get asset(): string { 95 | return this._asset; 96 | } 97 | 98 | // Control methods 99 | /** 100 | * Starts playing the particle system 101 | */ 102 | play() { 103 | if (this.component) { 104 | this.component.play(); 105 | } 106 | } 107 | 108 | /** 109 | * Pauses the particle system 110 | */ 111 | pause() { 112 | if (this.component) { 113 | this.component.pause(); 114 | } 115 | } 116 | 117 | /** 118 | * Resets the particle system 119 | */ 120 | reset() { 121 | if (this.component) { 122 | this.component.reset(); 123 | } 124 | } 125 | 126 | /** 127 | * Stops the particle system 128 | */ 129 | stop() { 130 | if (this.component) { 131 | this.component.stop(); 132 | } 133 | } 134 | 135 | static get observedAttributes() { 136 | return [ 137 | ...super.observedAttributes, 138 | 'asset' 139 | ]; 140 | } 141 | 142 | attributeChangedCallback(name: string, _oldValue: string, newValue: string) { 143 | super.attributeChangedCallback(name, _oldValue, newValue); 144 | 145 | switch (name) { 146 | case 'asset': 147 | this.asset = newValue; 148 | break; 149 | } 150 | } 151 | } 152 | 153 | customElements.define('pc-particles', ParticleSystemComponentElement); 154 | 155 | export { ParticleSystemComponentElement }; 156 | -------------------------------------------------------------------------------- /src/components/render-component.ts: -------------------------------------------------------------------------------- 1 | import { RenderComponent, StandardMaterial } from 'playcanvas'; 2 | 3 | import { ComponentElement } from './component'; 4 | import { MaterialElement } from '../material'; 5 | 6 | /** 7 | * The RenderComponentElement interface provides properties and methods for manipulating 8 | * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-render/ | ``} elements. 9 | * The RenderComponentElement interface also inherits the properties and methods of the 10 | * {@link HTMLElement} interface. 11 | * 12 | * @category Components 13 | */ 14 | class RenderComponentElement extends ComponentElement { 15 | private _castShadows = true; 16 | 17 | private _material: string = ''; 18 | 19 | private _receiveShadows = true; 20 | 21 | private _type: 'asset' | 'box' | 'capsule' | 'cone' | 'cylinder' | 'plane' | 'sphere' = 'asset'; 22 | 23 | /** @ignore */ 24 | constructor() { 25 | super('render'); 26 | } 27 | 28 | getInitialComponentData() { 29 | return { 30 | type: this._type, 31 | castShadows: this._castShadows, 32 | material: MaterialElement.get(this._material), 33 | receiveShadows: this._receiveShadows 34 | }; 35 | } 36 | 37 | /** 38 | * Gets the underlying PlayCanvas render component. 39 | * @returns The render component. 40 | */ 41 | get component(): RenderComponent | null { 42 | return super.component as RenderComponent | null; 43 | } 44 | 45 | /** 46 | * Sets the type of the render component. 47 | * @param value - The type. 48 | */ 49 | set type(value: 'asset' | 'box' | 'capsule' | 'cone' | 'cylinder' | 'plane' | 'sphere') { 50 | this._type = value; 51 | if (this.component) { 52 | this.component.type = value; 53 | } 54 | } 55 | 56 | /** 57 | * Gets the type of the render component. 58 | * @returns The type. 59 | */ 60 | get type(): 'asset' | 'box' | 'capsule' | 'cone' | 'cylinder' | 'plane' | 'sphere' { 61 | return this._type; 62 | } 63 | 64 | /** 65 | * Sets the cast shadows flag of the render component. 66 | * @param value - The cast shadows flag. 67 | */ 68 | set castShadows(value: boolean) { 69 | this._castShadows = value; 70 | if (this.component) { 71 | this.component.castShadows = value; 72 | } 73 | } 74 | 75 | /** 76 | * Gets the cast shadows flag of the render component. 77 | * @returns The cast shadows flag. 78 | */ 79 | get castShadows(): boolean { 80 | return this._castShadows; 81 | } 82 | 83 | /** 84 | * Sets the material of the render component. 85 | * @param value - The id of the material asset to use. 86 | */ 87 | set material(value: string) { 88 | this._material = value; 89 | if (this.component) { 90 | this.component.material = MaterialElement.get(value) as StandardMaterial; 91 | } 92 | } 93 | 94 | /** 95 | * Gets the id of the material asset used by the render component. 96 | * @returns The id of the material asset. 97 | */ 98 | get material() { 99 | return this._material; 100 | } 101 | 102 | /** 103 | * Sets the receive shadows flag of the render component. 104 | * @param value - The receive shadows flag. 105 | */ 106 | set receiveShadows(value: boolean) { 107 | this._receiveShadows = value; 108 | if (this.component) { 109 | this.component.receiveShadows = value; 110 | } 111 | } 112 | 113 | /** 114 | * Gets the receive shadows flag of the render component. 115 | * @returns The receive shadows flag. 116 | */ 117 | get receiveShadows(): boolean { 118 | return this._receiveShadows; 119 | } 120 | 121 | static get observedAttributes() { 122 | return [...super.observedAttributes, 'cast-shadows', 'material', 'receive-shadows', 'type']; 123 | } 124 | 125 | attributeChangedCallback(name: string, _oldValue: string, newValue: string) { 126 | super.attributeChangedCallback(name, _oldValue, newValue); 127 | 128 | switch (name) { 129 | case 'cast-shadows': 130 | this.castShadows = newValue !== 'false'; 131 | break; 132 | case 'material': 133 | this.material = newValue; 134 | break; 135 | case 'receive-shadows': 136 | this.receiveShadows = newValue !== 'false'; 137 | break; 138 | case 'type': 139 | this.type = newValue as 'asset' | 'box' | 'capsule' | 'cone' | 'cylinder' | 'plane' | 'sphere'; 140 | break; 141 | } 142 | } 143 | } 144 | 145 | customElements.define('pc-render', RenderComponentElement); 146 | 147 | export { RenderComponentElement }; 148 | -------------------------------------------------------------------------------- /src/components/screen-component.ts: -------------------------------------------------------------------------------- 1 | import { SCALEMODE_BLEND, SCALEMODE_NONE, ScreenComponent, Vec2 } from 'playcanvas'; 2 | 3 | import { ComponentElement } from './component'; 4 | import { parseVec2 } from '../utils'; 5 | 6 | /** 7 | * The ScreenComponentElement interface provides properties and methods for manipulating 8 | * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-screen/ | ``} elements. 9 | * The ScreenComponentElement interface also inherits the properties and methods of the 10 | * {@link HTMLElement} interface. 11 | * 12 | * @category Components 13 | */ 14 | class ScreenComponentElement extends ComponentElement { 15 | private _screenSpace = false; 16 | 17 | private _resolution: Vec2 = new Vec2(640, 320); 18 | 19 | private _referenceResolution: Vec2 = new Vec2(640, 320); 20 | 21 | private _priority = 0; 22 | 23 | private _blend = false; 24 | 25 | private _scaleBlend = 0.5; 26 | 27 | /** @ignore */ 28 | constructor() { 29 | super('screen'); 30 | } 31 | 32 | getInitialComponentData() { 33 | return { 34 | priority: this._priority, 35 | referenceResolution: this._referenceResolution, 36 | resolution: this._resolution, 37 | scaleBlend: this._scaleBlend, 38 | scaleMode: this._blend ? SCALEMODE_BLEND : SCALEMODE_NONE, 39 | screenSpace: this._screenSpace 40 | }; 41 | } 42 | 43 | /** 44 | * Gets the underlying PlayCanvas screen component. 45 | * @returns The screen component. 46 | */ 47 | get component(): ScreenComponent | null { 48 | return super.component as ScreenComponent | null; 49 | } 50 | 51 | set priority(value: number) { 52 | this._priority = value; 53 | if (this.component) { 54 | this.component.priority = this._priority; 55 | } 56 | } 57 | 58 | get priority() { 59 | return this._priority; 60 | } 61 | 62 | set referenceResolution(value: Vec2) { 63 | this._referenceResolution = value; 64 | if (this.component) { 65 | this.component.referenceResolution = this._referenceResolution; 66 | } 67 | } 68 | 69 | get referenceResolution() { 70 | return this._referenceResolution; 71 | } 72 | 73 | set resolution(value: Vec2) { 74 | this._resolution = value; 75 | if (this.component) { 76 | this.component.resolution = this._resolution; 77 | } 78 | } 79 | 80 | get resolution() { 81 | return this._resolution; 82 | } 83 | 84 | set scaleBlend(value: number) { 85 | this._scaleBlend = value; 86 | if (this.component) { 87 | this.component.scaleBlend = this._scaleBlend; 88 | } 89 | } 90 | 91 | get scaleBlend() { 92 | return this._scaleBlend; 93 | } 94 | 95 | set blend(value: boolean) { 96 | this._blend = value; 97 | if (this.component) { 98 | this.component.scaleMode = this._blend ? SCALEMODE_BLEND : SCALEMODE_NONE; 99 | } 100 | } 101 | 102 | get blend() { 103 | return this._blend; 104 | } 105 | 106 | set screenSpace(value: boolean) { 107 | this._screenSpace = value; 108 | if (this.component) { 109 | this.component.screenSpace = this._screenSpace; 110 | } 111 | } 112 | 113 | get screenSpace() { 114 | return this._screenSpace; 115 | } 116 | 117 | static get observedAttributes() { 118 | return [ 119 | ...super.observedAttributes, 120 | 'blend', 121 | 'screen-space', 122 | 'resolution', 123 | 'reference-resolution', 124 | 'priority', 125 | 'scale-blend' 126 | ]; 127 | } 128 | 129 | attributeChangedCallback(name: string, _oldValue: string, newValue: string) { 130 | super.attributeChangedCallback(name, _oldValue, newValue); 131 | 132 | switch (name) { 133 | case 'priority': 134 | this.priority = parseInt(newValue, 10); 135 | break; 136 | case 'reference-resolution': 137 | this.referenceResolution = parseVec2(newValue); 138 | break; 139 | case 'resolution': 140 | this.resolution = parseVec2(newValue); 141 | break; 142 | case 'scale-blend': 143 | this.scaleBlend = parseFloat(newValue); 144 | break; 145 | case 'blend': 146 | this.blend = this.hasAttribute('blend'); 147 | break; 148 | case 'screen-space': 149 | this.screenSpace = this.hasAttribute('screen-space'); 150 | break; 151 | } 152 | } 153 | } 154 | 155 | customElements.define('pc-screen', ScreenComponentElement); 156 | 157 | export { ScreenComponentElement }; 158 | -------------------------------------------------------------------------------- /src/components/script.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The ScriptElement interface provides properties and methods for manipulating 3 | * `` elements. The ScriptElement interface also inherits the properties and 4 | * methods of the {@link HTMLElement} interface. 5 | */ 6 | class ScriptElement extends HTMLElement { 7 | private _attributes: string = '{}'; 8 | 9 | private _enabled: boolean = true; 10 | 11 | private _name: string = ''; 12 | 13 | /** 14 | * Sets the attributes of the script. 15 | * @param value - The attributes of the script. 16 | */ 17 | set scriptAttributes(value: string) { 18 | this._attributes = value; 19 | this.dispatchEvent(new CustomEvent('scriptattributeschange', { 20 | detail: { attributes: value }, 21 | bubbles: true 22 | })); 23 | } 24 | 25 | /** 26 | * Gets the attributes of the script. 27 | * @returns The attributes of the script. 28 | */ 29 | get scriptAttributes() { 30 | return this._attributes; 31 | } 32 | 33 | /** 34 | * Sets the enabled state of the script. 35 | * @param value - The enabled state of the script. 36 | */ 37 | set enabled(value: boolean) { 38 | this._enabled = value; 39 | this.dispatchEvent(new CustomEvent('scriptenablechange', { 40 | detail: { enabled: value }, 41 | bubbles: true 42 | })); 43 | } 44 | 45 | /** 46 | * Gets the enabled state of the script. 47 | * @returns The enabled state of the script. 48 | */ 49 | get enabled() { 50 | return this._enabled; 51 | } 52 | 53 | /** 54 | * Sets the name of the script to create. 55 | * @param value - The name. 56 | */ 57 | set name(value: string) { 58 | this._name = value; 59 | } 60 | 61 | /** 62 | * Gets the name of the script. 63 | * @returns The name. 64 | */ 65 | get name() { 66 | return this._name; 67 | } 68 | 69 | static get observedAttributes() { 70 | return ['attributes', 'enabled', 'name']; 71 | } 72 | 73 | attributeChangedCallback(name: string, _oldValue: string, newValue: string) { 74 | switch (name) { 75 | case 'attributes': 76 | this.scriptAttributes = newValue; 77 | break; 78 | case 'enabled': 79 | this.enabled = newValue !== 'false'; 80 | break; 81 | case 'name': 82 | this.name = newValue; 83 | break; 84 | } 85 | } 86 | } 87 | 88 | customElements.define('pc-script', ScriptElement); 89 | 90 | export { ScriptElement }; 91 | -------------------------------------------------------------------------------- /src/components/splat-component.ts: -------------------------------------------------------------------------------- 1 | import { GSplatComponent } from 'playcanvas'; 2 | 3 | import { ComponentElement } from './component'; 4 | import { AssetElement } from '../asset'; 5 | 6 | /** 7 | * The SplatComponentElement interface provides properties and methods for manipulating 8 | * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-splat/ | ``} elements. 9 | * The SplatComponentElement interface also inherits the properties and methods of the 10 | * {@link HTMLElement} interface. 11 | * 12 | * @category Components 13 | */ 14 | class SplatComponentElement extends ComponentElement { 15 | private _asset = ''; 16 | 17 | private _castShadows = false; 18 | 19 | /** @ignore */ 20 | constructor() { 21 | super('gsplat'); 22 | } 23 | 24 | getInitialComponentData() { 25 | return { 26 | asset: AssetElement.get(this._asset), 27 | castShadows: this._castShadows 28 | }; 29 | } 30 | 31 | /** 32 | * Gets the underlying PlayCanvas splat component. 33 | * @returns The splat component. 34 | */ 35 | get component(): GSplatComponent | null { 36 | return super.component as GSplatComponent | null; 37 | } 38 | 39 | /** 40 | * Sets id of the `pc-asset` to use for the splat. 41 | * @param value - The asset ID. 42 | */ 43 | set asset(value: string) { 44 | this._asset = value; 45 | const asset = AssetElement.get(value); 46 | if (this.component && asset) { 47 | this.component.asset = asset; 48 | } 49 | } 50 | 51 | /** 52 | * Gets the id of the `pc-asset` to use for the splat. 53 | * @returns The asset ID. 54 | */ 55 | get asset() { 56 | return this._asset; 57 | } 58 | 59 | /** 60 | * Sets whether the splat casts shadows. 61 | * @param value - Whether the splat casts shadows. 62 | */ 63 | set castShadows(value: boolean) { 64 | this._castShadows = value; 65 | if (this.component) { 66 | this.component.castShadows = value; 67 | } 68 | } 69 | 70 | /** 71 | * Gets whether the splat casts shadows. 72 | * @returns Whether the splat casts shadows. 73 | */ 74 | get castShadows() { 75 | return this._castShadows; 76 | } 77 | 78 | static get observedAttributes() { 79 | return [ 80 | ...super.observedAttributes, 81 | 'asset', 82 | 'cast-shadows' 83 | ]; 84 | } 85 | 86 | attributeChangedCallback(name: string, _oldValue: string, newValue: string) { 87 | super.attributeChangedCallback(name, _oldValue, newValue); 88 | 89 | switch (name) { 90 | case 'asset': 91 | this.asset = newValue; 92 | break; 93 | case 'cast-shadows': 94 | this.castShadows = this.hasAttribute('cast-shadows'); 95 | break; 96 | } 97 | } 98 | } 99 | 100 | customElements.define('pc-splat', SplatComponentElement); 101 | 102 | export { SplatComponentElement }; 103 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The Engine Web Components module provides a set of Web Components for the PlayCanvas Engine. 3 | * While these components are normally instantiated in a declarative fashion using HTML, this 4 | * reference covers the TypeScript/JavaScript API that allows these components to be created 5 | * programmatically. 6 | * 7 | * @module EngineWebComponents 8 | */ 9 | 10 | /* eslint-disable import/order */ 11 | 12 | // Note that order matters here (e.g. pc-entity must be defined before components) 13 | import { AsyncElement } from './async-element'; 14 | import { ModuleElement } from './module'; 15 | import { AppElement } from './app'; 16 | import { EntityElement } from './entity'; 17 | import { AssetElement } from './asset'; 18 | import { ListenerComponentElement } from './components/listener-component'; 19 | import { CameraComponentElement } from './components/camera-component'; 20 | import { CollisionComponentElement } from './components/collision-component'; 21 | import { ComponentElement } from './components/component'; 22 | import { ElementComponentElement } from './components/element-component'; 23 | import { LightComponentElement } from './components/light-component'; 24 | import { ParticleSystemComponentElement } from './components/particlesystem-component'; 25 | import { RenderComponentElement } from './components/render-component'; 26 | import { RigidBodyComponentElement } from './components/rigidbody-component'; 27 | import { ScreenComponentElement } from './components/screen-component'; 28 | import { ScriptComponentElement } from './components/script-component'; 29 | import { ScriptElement } from './components/script'; 30 | import { SoundComponentElement } from './components/sound-component'; 31 | import { SoundSlotElement } from './components/sound-slot'; 32 | import { SplatComponentElement } from './components/splat-component'; 33 | import { MaterialElement } from './material'; 34 | import { ModelElement } from './model'; 35 | import { SceneElement } from './scene'; 36 | import { SkyElement } from './sky'; 37 | 38 | export { 39 | AsyncElement, 40 | ModuleElement, 41 | AppElement, 42 | EntityElement, 43 | AssetElement, 44 | CameraComponentElement, 45 | CollisionComponentElement, 46 | ComponentElement, 47 | ElementComponentElement, 48 | ParticleSystemComponentElement, 49 | LightComponentElement, 50 | ListenerComponentElement, 51 | RenderComponentElement, 52 | RigidBodyComponentElement, 53 | ScreenComponentElement, 54 | ScriptComponentElement, 55 | ScriptElement, 56 | SoundComponentElement, 57 | SoundSlotElement, 58 | SplatComponentElement, 59 | MaterialElement, 60 | ModelElement, 61 | SceneElement, 62 | SkyElement 63 | }; 64 | -------------------------------------------------------------------------------- /src/material.ts: -------------------------------------------------------------------------------- 1 | import { Color, StandardMaterial, Texture } from 'playcanvas'; 2 | 3 | import { AssetElement } from './asset'; 4 | import { parseColor } from './utils'; 5 | 6 | /** 7 | * The MaterialElement interface provides properties and methods for manipulating 8 | * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-material/ | ``} elements. 9 | * The MaterialElement interface also inherits the properties and methods of the 10 | * {@link HTMLElement} interface. 11 | */ 12 | class MaterialElement extends HTMLElement { 13 | private _diffuse = new Color(1, 1, 1); 14 | 15 | private _diffuseMap = ''; 16 | 17 | private _metalnessMap = ''; 18 | 19 | private _normalMap = ''; 20 | 21 | private _roughnessMap = ''; 22 | 23 | material: StandardMaterial | null = null; 24 | 25 | createMaterial() { 26 | this.material = new StandardMaterial(); 27 | this.material.glossInvert = false; 28 | this.material.useMetalness = false; 29 | this.material.diffuse = this._diffuse; 30 | this.diffuseMap = this._diffuseMap; 31 | this.metalnessMap = this._metalnessMap; 32 | this.normalMap = this._normalMap; 33 | this.roughnessMap = this._roughnessMap; 34 | this.material.update(); 35 | } 36 | 37 | disconnectedCallback() { 38 | if (this.material) { 39 | this.material.destroy(); 40 | this.material = null; 41 | } 42 | } 43 | 44 | setMap(map: string, property: 'diffuseMap' | 'metalnessMap' | 'normalMap' | 'glossMap') { 45 | if (this.material) { 46 | const asset = AssetElement.get(map); 47 | if (asset) { 48 | if (asset.loaded) { 49 | this.material[property] = asset.resource as Texture; 50 | this.material[property]!.anisotropy = 4; 51 | } else { 52 | asset.once('load', () => { 53 | this.material![property] = asset.resource as Texture; 54 | this.material![property]!.anisotropy = 4; 55 | this.material!.update(); 56 | }); 57 | } 58 | } 59 | } 60 | } 61 | 62 | set diffuse(value: Color) { 63 | this._diffuse = value; 64 | if (this.material) { 65 | this.material.diffuse = value; 66 | } 67 | } 68 | 69 | get diffuse(): Color { 70 | return this._diffuse; 71 | } 72 | 73 | set diffuseMap(value: string) { 74 | this._diffuseMap = value; 75 | this.setMap(value, 'diffuseMap'); 76 | } 77 | 78 | get diffuseMap() { 79 | return this._diffuseMap; 80 | } 81 | 82 | set metalnessMap(value: string) { 83 | this._metalnessMap = value; 84 | this.setMap(value, 'metalnessMap'); 85 | } 86 | 87 | get metalnessMap() { 88 | return this._metalnessMap; 89 | } 90 | 91 | set normalMap(value: string) { 92 | this._normalMap = value; 93 | this.setMap(value, 'normalMap'); 94 | } 95 | 96 | get normalMap() { 97 | return this._normalMap; 98 | } 99 | 100 | set roughnessMap(value: string) { 101 | this._roughnessMap = value; 102 | this.setMap(value, 'glossMap'); 103 | } 104 | 105 | get roughnessMap() { 106 | return this._roughnessMap; 107 | } 108 | 109 | static get(id: string) { 110 | const materialElement = document.querySelector(`pc-material[id="${id}"]`); 111 | return materialElement?.material; 112 | } 113 | 114 | static get observedAttributes() { 115 | return ['diffuse', 'diffuse-map', 'metalness-map', 'normal-map', 'roughness-map']; 116 | } 117 | 118 | attributeChangedCallback(name: string, _oldValue: string, newValue: string) { 119 | switch (name) { 120 | case 'diffuse': 121 | this.diffuse = parseColor(newValue); 122 | break; 123 | case 'diffuse-map': 124 | this.diffuseMap = newValue; 125 | break; 126 | case 'metalness-map': 127 | this.metalnessMap = newValue; 128 | break; 129 | case 'normal-map': 130 | this.normalMap = newValue; 131 | break; 132 | case 'roughness-map': 133 | this.roughnessMap = newValue; 134 | break; 135 | } 136 | } 137 | } 138 | 139 | customElements.define('pc-material', MaterialElement); 140 | 141 | export { MaterialElement }; 142 | -------------------------------------------------------------------------------- /src/model.ts: -------------------------------------------------------------------------------- 1 | import { ContainerResource, Entity } from 'playcanvas'; 2 | 3 | import { AssetElement } from './asset'; 4 | import { AsyncElement } from './async-element'; 5 | 6 | /** 7 | * The ModelElement interface provides properties and methods for manipulating 8 | * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-model/ | ``} elements. 9 | * The ModelElement interface also inherits the properties and methods of the 10 | * {@link HTMLElement} interface. 11 | */ 12 | class ModelElement extends AsyncElement { 13 | private _asset: string = ''; 14 | 15 | private _entity: Entity | null = null; 16 | 17 | connectedCallback() { 18 | this._loadModel(); 19 | this._onReady(); 20 | } 21 | 22 | disconnectedCallback() { 23 | this._unloadModel(); 24 | } 25 | 26 | private _instantiate(container: ContainerResource) { 27 | this._entity = container.instantiateRenderEntity(); 28 | 29 | // @ts-ignore 30 | if (container.animations.length > 0) { 31 | this._entity.addComponent('anim'); 32 | // @ts-ignore 33 | this._entity.anim.assignAnimation('animation', container.animations[0].resource); 34 | } 35 | 36 | const parentEntityElement = this.closestEntity; 37 | if (parentEntityElement) { 38 | parentEntityElement.ready().then(() => { 39 | parentEntityElement.entity!.addChild(this._entity!); 40 | }); 41 | } else { 42 | const appElement = this.closestApp; 43 | if (appElement) { 44 | appElement.ready().then(() => { 45 | appElement.app!.root.addChild(this._entity!); 46 | }); 47 | } 48 | } 49 | } 50 | 51 | private async _loadModel() { 52 | this._unloadModel(); 53 | 54 | const appElement = await this.closestApp?.ready(); 55 | const app = appElement?.app; 56 | 57 | const asset = AssetElement.get(this._asset); 58 | if (!asset) { 59 | return; 60 | } 61 | 62 | if (asset.loaded) { 63 | this._instantiate(asset.resource as ContainerResource); 64 | } else { 65 | asset.once('load', () => { 66 | this._instantiate(asset.resource as ContainerResource); 67 | }); 68 | app!.assets.load(asset); 69 | } 70 | } 71 | 72 | private _unloadModel() { 73 | this._entity?.destroy(); 74 | this._entity = null; 75 | } 76 | 77 | /** 78 | * Sets the id of the `pc-asset` to use for the model. 79 | * @param value - The asset ID. 80 | */ 81 | set asset(value: string) { 82 | this._asset = value; 83 | if (this.isConnected) { 84 | this._loadModel(); 85 | } 86 | } 87 | 88 | /** 89 | * Gets the id of the `pc-asset` to use for the model. 90 | * @returns The asset ID. 91 | */ 92 | get asset(): string { 93 | return this._asset; 94 | } 95 | 96 | static get observedAttributes() { 97 | return ['asset']; 98 | } 99 | 100 | attributeChangedCallback(name: string, _oldValue: string, newValue: string) { 101 | switch (name) { 102 | case 'asset': 103 | this.asset = newValue; 104 | break; 105 | } 106 | } 107 | } 108 | 109 | customElements.define('pc-model', ModelElement); 110 | 111 | export { ModelElement }; 112 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { basisInitialize, WasmModule } from 'playcanvas'; 2 | 3 | /** 4 | * The ModuleElement interface provides properties and methods for manipulating 5 | * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-module/ | ``} elements. 6 | * The ModuleElement interface also inherits the properties and methods of the 7 | * {@link HTMLElement} interface. 8 | */ 9 | class ModuleElement extends HTMLElement { 10 | private loadPromise: Promise; 11 | 12 | /** @ignore */ 13 | constructor() { 14 | super(); 15 | this.loadPromise = this.loadModule(); 16 | } 17 | 18 | private async loadModule(): Promise { 19 | const name = this.getAttribute('name')!; 20 | const glueUrl = this.getAttribute('glue')!; 21 | const wasmUrl = this.getAttribute('wasm')!; 22 | const fallbackUrl = this.getAttribute('fallback')!; 23 | const config = { glueUrl, wasmUrl, fallbackUrl }; 24 | 25 | if (name === 'Basis') { 26 | basisInitialize(config); 27 | } else { 28 | WasmModule.setConfig(name, config); 29 | 30 | await new Promise((resolve) => { 31 | WasmModule.getInstance(name, () => resolve()); 32 | }); 33 | } 34 | } 35 | 36 | public getLoadPromise(): Promise { 37 | return this.loadPromise; 38 | } 39 | } 40 | 41 | customElements.define('pc-module', ModuleElement); 42 | 43 | export { ModuleElement }; 44 | -------------------------------------------------------------------------------- /src/scene.ts: -------------------------------------------------------------------------------- 1 | import { Color, Scene, Vec3 } from 'playcanvas'; 2 | 3 | import { AppElement } from './app'; 4 | import { AsyncElement } from './async-element'; 5 | import { parseColor, parseVec3 } from './utils'; 6 | 7 | /** 8 | * The SceneElement interface provides properties and methods for manipulating 9 | * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-scene/ | ``} elements. 10 | * The SceneElement interface also inherits the properties and methods of the 11 | * {@link HTMLElement} interface. 12 | */ 13 | class SceneElement extends AsyncElement { 14 | /** 15 | * The fog type of the scene. 16 | */ 17 | private _fog = 'none'; // possible values: 'none', 'linear', 'exp', 'exp2' 18 | 19 | /** 20 | * The color of the fog. 21 | */ 22 | private _fogColor = new Color(1, 1, 1); 23 | 24 | /** 25 | * The density of the fog. 26 | */ 27 | private _fogDensity = 0; 28 | 29 | /** 30 | * The start distance of the fog. 31 | */ 32 | private _fogStart = 0; 33 | 34 | /** 35 | * The end distance of the fog. 36 | */ 37 | private _fogEnd = 1000; 38 | 39 | /** 40 | * The gravity of the scene. 41 | */ 42 | private _gravity = new Vec3(0, -9.81, 0); 43 | 44 | /** 45 | * The PlayCanvas scene instance. 46 | */ 47 | scene: Scene | null = null; 48 | 49 | async connectedCallback() { 50 | await this.closestApp?.ready(); 51 | 52 | this.scene = this.closestApp!.app!.scene; 53 | this.updateSceneSettings(); 54 | 55 | this._onReady(); 56 | } 57 | 58 | updateSceneSettings() { 59 | if (this.scene) { 60 | this.scene.fog.type = this._fog; 61 | this.scene.fog.color = this._fogColor; 62 | this.scene.fog.density = this._fogDensity; 63 | this.scene.fog.start = this._fogStart; 64 | this.scene.fog.end = this._fogEnd; 65 | 66 | const appElement = this.parentElement as AppElement; 67 | appElement.app!.systems.rigidbody!.gravity.copy(this._gravity); 68 | } 69 | } 70 | 71 | /** 72 | * Sets the fog type of the scene. 73 | * @param value - The fog type. 74 | */ 75 | set fog(value) { 76 | this._fog = value; 77 | if (this.scene) { 78 | this.scene.fog.type = value; 79 | } 80 | } 81 | 82 | /** 83 | * Gets the fog type of the scene. 84 | * @returns The fog type. 85 | */ 86 | get fog() { 87 | return this._fog; 88 | } 89 | 90 | /** 91 | * Sets the fog color of the scene. 92 | * @param value - The fog color. 93 | */ 94 | set fogColor(value: Color) { 95 | this._fogColor = value; 96 | if (this.scene) { 97 | this.scene.fog.color = value; 98 | } 99 | } 100 | 101 | /** 102 | * Gets the fog color of the scene. 103 | * @returns The fog color. 104 | */ 105 | get fogColor() { 106 | return this._fogColor; 107 | } 108 | 109 | /** 110 | * Sets the fog density of the scene. 111 | * @param value - The fog density. 112 | */ 113 | set fogDensity(value: number) { 114 | this._fogDensity = value; 115 | if (this.scene) { 116 | this.scene.fog.density = value; 117 | } 118 | } 119 | 120 | /** 121 | * Gets the fog density of the scene. 122 | * @returns The fog density. 123 | */ 124 | get fogDensity() { 125 | return this._fogDensity; 126 | } 127 | 128 | /** 129 | * Sets the fog start distance of the scene. 130 | * @param value - The fog start distance. 131 | */ 132 | set fogStart(value: number) { 133 | this._fogStart = value; 134 | if (this.scene) { 135 | this.scene.fog.start = value; 136 | } 137 | } 138 | 139 | /** 140 | * Gets the fog start distance of the scene. 141 | * @returns The fog start distance. 142 | */ 143 | get fogStart() { 144 | return this._fogStart; 145 | } 146 | 147 | /** 148 | * Sets the fog end distance of the scene. 149 | * @param value - The fog end distance. 150 | */ 151 | set fogEnd(value: number) { 152 | this._fogEnd = value; 153 | if (this.scene) { 154 | this.scene.fog.end = value; 155 | } 156 | } 157 | 158 | /** 159 | * Gets the fog end distance of the scene. 160 | * @returns The fog end distance. 161 | */ 162 | get fogEnd() { 163 | return this._fogEnd; 164 | } 165 | 166 | /** 167 | * Sets the gravity of the scene. 168 | * @param value - The gravity. 169 | */ 170 | set gravity(value: Vec3) { 171 | this._gravity = value; 172 | if (this.scene) { 173 | const appElement = this.parentElement as AppElement; 174 | appElement.app!.systems.rigidbody!.gravity.copy(value); 175 | } 176 | } 177 | 178 | /** 179 | * Gets the gravity of the scene. 180 | * @returns The gravity. 181 | */ 182 | get gravity() { 183 | return this._gravity; 184 | } 185 | 186 | static get observedAttributes() { 187 | return ['fog', 'fog-color', 'fog-density', 'fog-start', 'fog-end', 'gravity']; 188 | } 189 | 190 | attributeChangedCallback(name: string, _oldValue: string, newValue: string) { 191 | switch (name) { 192 | case 'fog': 193 | this.fog = newValue; 194 | break; 195 | case 'fog-color': 196 | this.fogColor = parseColor(newValue); 197 | break; 198 | case 'fog-density': 199 | this.fogDensity = parseFloat(newValue); 200 | break; 201 | case 'fog-start': 202 | this.fogStart = parseFloat(newValue); 203 | break; 204 | case 'fog-end': 205 | this.fogEnd = parseFloat(newValue); 206 | break; 207 | case 'gravity': 208 | this.gravity = parseVec3(newValue); 209 | break; 210 | // ... handle other attributes as well 211 | } 212 | } 213 | } 214 | 215 | customElements.define('pc-scene', SceneElement); 216 | 217 | export { SceneElement }; 218 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Color, Quat, Vec2, Vec3, Vec4 } from 'playcanvas'; 2 | 3 | import { CSS_COLORS } from './colors'; 4 | 5 | /** 6 | * Parse a color string into a Color object. String can be in the format of '#rgb', '#rgba', 7 | * '#rrggbb', '#rrggbbaa', or a string of 3 or 4 comma-delimited numbers. 8 | * 9 | * @param value - The color string to parse. 10 | * @returns The parsed Color object. 11 | */ 12 | export const parseColor = (value: string): Color => { 13 | // Check if it's a CSS color name first 14 | const hexColor = CSS_COLORS[value.toLowerCase()]; 15 | if (hexColor) { 16 | return new Color().fromString(hexColor); 17 | } 18 | 19 | if (value.startsWith('#')) { 20 | return new Color().fromString(value); 21 | } 22 | 23 | const components = value.split(' ').map(Number); 24 | return new Color(components); 25 | }; 26 | 27 | /** 28 | * Parse an Euler angles string into a Quat object. String can be in the format of 'x,y,z'. 29 | * 30 | * @param value - The Euler angles string to parse. 31 | * @returns The parsed Quat object. 32 | */ 33 | export const parseQuat = (value: string): Quat => { 34 | const [x, y, z] = value.split(' ').map(Number); 35 | const q = new Quat(); 36 | q.setFromEulerAngles(x, y, z); 37 | return q; 38 | }; 39 | 40 | /** 41 | * Parse a Vec2 string into a Vec2 object. String can be in the format of 'x,y'. 42 | * 43 | * @param value - The Vec2 string to parse. 44 | * @returns The parsed Vec2 object. 45 | */ 46 | export const parseVec2 = (value: string): Vec2 => { 47 | const components = value.split(' ').map(Number); 48 | return new Vec2(components); 49 | }; 50 | 51 | /** 52 | * Parse a Vec3 string into a Vec3 object. String can be in the format of 'x,y,z'. 53 | * 54 | * @param value - The Vec3 string to parse. 55 | * @returns The parsed Vec3 object. 56 | */ 57 | export const parseVec3 = (value: string): Vec3 => { 58 | const components = value.split(' ').map(Number); 59 | return new Vec3(components); 60 | }; 61 | 62 | /** 63 | * Parse a Vec4 string into a Vec4 object. String can be in the format of 'x,y,z,w'. 64 | * 65 | * @param value - The Vec4 string to parse. 66 | * @returns The parsed Vec4 object. 67 | */ 68 | export const parseVec4 = (value: string): Vec4 => { 69 | const components = value.split(' ').map(Number); 70 | return new Vec4(components); 71 | }; 72 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "ESNext", 5 | "lib": [ 6 | "ES2017", 7 | "DOM", 8 | "DOM.Iterable" 9 | ], 10 | "declaration": true, 11 | "declarationDir": "./dist", 12 | "outDir": "./dist", 13 | "rootDir": "./src", 14 | "strict": true, 15 | "moduleResolution": "node", 16 | "esModuleInterop": true, 17 | "skipLibCheck": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "sourceMap": true, 20 | "removeComments": false, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noImplicitReturns": true, 24 | "noFallthroughCasesInSwitch": true 25 | }, 26 | "include": [ 27 | "src/**/*" 28 | ], 29 | "exclude": [ 30 | "node_modules", 31 | "dist" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "entryPoints": [ 4 | "./src/index.ts" 5 | ], 6 | "exclude": [ 7 | "**/node_modules/**" 8 | ], 9 | "excludeNotDocumented": true, 10 | "externalSymbolLinkMappings": { 11 | "playcanvas": { 12 | "Application": "https://api.playcanvas.com/engine/classes/Application.html", 13 | "Asset": "https://api.playcanvas.com/engine/classes/Asset.html", 14 | "AudioListenerComponent": "https://api.playcanvas.com/engine/classes/AudioListenerComponent.html", 15 | "CameraComponent": "https://api.playcanvas.com/engine/classes/CameraComponent.html", 16 | "CollisionComponent": "https://api.playcanvas.com/engine/classes/CollisionComponent.html", 17 | "Color": "https://api.playcanvas.com/engine/classes/Color.html", 18 | "ElementComponent": "https://api.playcanvas.com/engine/classes/ElementComponent.html", 19 | "Entity": "https://api.playcanvas.com/engine/classes/Entity.html", 20 | "GSplatComponent": "https://api.playcanvas.com/engine/classes/GSplatComponent.html", 21 | "LightComponent": "https://api.playcanvas.com/engine/classes/LightComponent.html", 22 | "ParticleSystemComponent": "https://api.playcanvas.com/engine/classes/ParticleSystemComponent.html", 23 | "RenderComponent": "https://api.playcanvas.com/engine/classes/RenderComponent.html", 24 | "RigidBodyComponent": "https://api.playcanvas.com/engine/classes/RigidBodyComponent.html", 25 | "Scene": "https://api.playcanvas.com/engine/classes/Scene.html", 26 | "ScreenComponent": "https://api.playcanvas.com/engine/classes/ScreenComponent.html", 27 | "ScriptComponent": "https://api.playcanvas.com/engine/classes/ScriptComponent.html", 28 | "SoundComponent": "https://api.playcanvas.com/engine/classes/SoundComponent.html", 29 | "SoundSlot": "https://api.playcanvas.com/engine/classes/SoundSlot.html", 30 | "Vec2": "https://api.playcanvas.com/engine/classes/Vec2.html", 31 | "Vec3": "https://api.playcanvas.com/engine/classes/Vec3.html", 32 | "Vec4": "https://api.playcanvas.com/engine/classes/Vec4.html" 33 | } 34 | }, 35 | "favicon": "utils/typedoc/favicon.ico", 36 | "hostedBaseUrl": "https://api.playcanvas.com/web-components/", 37 | "includeVersion": true, 38 | "name": "Web Components API Reference", 39 | "navigationLinks": { 40 | "Developer Site": "https://developer.playcanvas.com/", 41 | "Blog": "https://blog.playcanvas.com/", 42 | "Discord": "https://discord.gg/RSaMRzg", 43 | "Forum": "https://forum.playcanvas.com/", 44 | "GitHub": "https://github.com/playcanvas/web-components" 45 | }, 46 | "sidebarLinks": { 47 | "Home": "/" 48 | }, 49 | "plugin": [ 50 | "typedoc-plugin-mdn-links" 51 | ], 52 | "readme": "none", 53 | "searchGroupBoosts": { 54 | "Classes": 2 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /utils/typedoc/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playcanvas/web-components/075dc52df4b6479bad5f2fce76d0422fdf81a821/utils/typedoc/favicon.ico --------------------------------------------------------------------------------