├── .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 | [](https://www.npmjs.com/package/@playcanvas/web-components)
4 | [](https://npmtrends.com/@playcanvas/web-components)
5 | [](https://github.com/playcanvas/web-components/blob/main/LICENSE)
6 | [](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 | [](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 |
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 | `;
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 | `;
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 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
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
--------------------------------------------------------------------------------