create-playcanvas 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +37 -25
  2. package/dist/index.mjs +48 -46
  3. package/package.json +2 -1
  4. package/templates/_shared/model-viewer/public/playcanvas-cube.glb +0 -0
  5. package/templates/{vanilla-ts → engine/base}/README.md +3 -19
  6. package/templates/{vanilla-ts → engine/base}/src/vite-env.d.ts +6 -0
  7. package/templates/{vanilla-ts → engine/interactive-sphere}/src/main.ts +1 -1
  8. package/templates/{vanilla-ts → engine/interactive-sphere}/src/style.css +1 -1
  9. package/templates/{vanilla-ts → engine/interactive-sphere}/src/utils.ts +0 -1
  10. package/templates/engine/model-viewer/index.html +13 -0
  11. package/templates/engine/model-viewer/src/main.ts +81 -0
  12. package/templates/engine/model-viewer/src/style.css +8 -0
  13. package/templates/engine/spinning-cube/index.html +13 -0
  14. package/templates/engine/spinning-cube/src/main.ts +65 -0
  15. package/templates/engine/spinning-cube/src/style.css +8 -0
  16. package/templates/engine/splat-viewer/index.html +13 -0
  17. package/templates/engine/splat-viewer/src/main.ts +74 -0
  18. package/templates/engine/splat-viewer/src/style.css +8 -0
  19. package/templates/index.ts +37 -5
  20. package/templates/{react-ts → react/base}/README.md +2 -19
  21. package/templates/{react-ts → react/base}/src/vite-env.d.ts +6 -0
  22. package/templates/{react-ts → react/interactive-sphere}/src/Scene.tsx +1 -2
  23. package/templates/react/model-viewer/index.html +13 -0
  24. package/templates/react/model-viewer/src/App.tsx +43 -0
  25. package/templates/react/model-viewer/src/index.css +9 -0
  26. package/templates/react/model-viewer/src/main.tsx +11 -0
  27. package/templates/react/spinning-cube/index.html +13 -0
  28. package/templates/react/spinning-cube/src/App.tsx +36 -0
  29. package/templates/react/spinning-cube/src/index.css +9 -0
  30. package/templates/react/spinning-cube/src/main.tsx +11 -0
  31. package/templates/react/splat-viewer/index.html +13 -0
  32. package/templates/react/splat-viewer/src/App.tsx +39 -0
  33. package/templates/react/splat-viewer/src/index.css +9 -0
  34. package/templates/react/splat-viewer/src/main.tsx +11 -0
  35. package/templates/web-components/base/README.md +66 -0
  36. package/templates/web-components/base/_gitignore +24 -0
  37. package/templates/web-components/base/eslint.config.mjs +3 -0
  38. package/templates/web-components/base/package.json +31 -0
  39. package/templates/web-components/base/prettier.config.mjs +1 -0
  40. package/templates/web-components/base/src/assets/playcanvas.png +0 -0
  41. package/templates/web-components/base/src/vite-env.d.ts +19 -0
  42. package/templates/web-components/base/tsconfig.json +25 -0
  43. package/templates/web-components/interactive-sphere/index.html +53 -0
  44. package/templates/web-components/interactive-sphere/src/assets/typescript.svg +1 -0
  45. package/templates/web-components/interactive-sphere/src/main.ts +76 -0
  46. package/templates/web-components/interactive-sphere/src/style.css +111 -0
  47. package/templates/web-components/model-viewer/index.html +34 -0
  48. package/templates/web-components/model-viewer/src/main.ts +26 -0
  49. package/templates/web-components/model-viewer/src/style.css +8 -0
  50. package/templates/web-components/spinning-cube/index.html +29 -0
  51. package/templates/web-components/spinning-cube/src/main.ts +20 -0
  52. package/templates/web-components/spinning-cube/src/style.css +8 -0
  53. package/templates/web-components/splat-viewer/index.html +31 -0
  54. package/templates/web-components/splat-viewer/src/main.ts +14 -0
  55. package/templates/web-components/splat-viewer/src/style.css +8 -0
  56. package/templates/vanilla-ts/public/environment-map.png +0 -0
  57. /package/templates/{react-ts → _shared/interactive-sphere}/public/environment-map.png +0 -0
  58. /package/templates/{react-ts → engine/base}/_gitignore +0 -0
  59. /package/templates/{vanilla-ts → engine/base}/eslint.config.mjs +0 -0
  60. /package/templates/{vanilla-ts → engine/base}/package.json +0 -0
  61. /package/templates/{react-ts → engine/base}/prettier.config.mjs +0 -0
  62. /package/templates/{react-ts → engine/base}/src/assets/playcanvas.png +0 -0
  63. /package/templates/{vanilla-ts → engine/base}/tsconfig.json +0 -0
  64. /package/templates/{vanilla-ts → engine/interactive-sphere}/index.html +0 -0
  65. /package/templates/{vanilla-ts → engine/interactive-sphere}/src/App.ts +0 -0
  66. /package/templates/{vanilla-ts → engine/interactive-sphere}/src/assets/typescript.svg +0 -0
  67. /package/templates/{vanilla-ts → react/base}/_gitignore +0 -0
  68. /package/templates/{react-ts → react/base}/eslint.config.mjs +0 -0
  69. /package/templates/{react-ts → react/base}/package.json +0 -0
  70. /package/templates/{vanilla-ts → react/base}/prettier.config.mjs +0 -0
  71. /package/templates/{vanilla-ts → react/base}/src/assets/playcanvas.png +0 -0
  72. /package/templates/{react-ts → react/base}/tsconfig.json +0 -0
  73. /package/templates/{react-ts → react/base}/vite.config.ts +0 -0
  74. /package/templates/{react-ts → react/interactive-sphere}/index.html +0 -0
  75. /package/templates/{react-ts → react/interactive-sphere}/src/App.css +0 -0
  76. /package/templates/{react-ts → react/interactive-sphere}/src/App.tsx +0 -0
  77. /package/templates/{react-ts → react/interactive-sphere}/src/assets/react.svg +0 -0
  78. /package/templates/{react-ts → react/interactive-sphere}/src/index.css +0 -0
  79. /package/templates/{react-ts → react/interactive-sphere}/src/main.tsx +0 -0
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/png" href="/src/assets/playcanvas.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>PlayCanvas + TS</title>
8
+ </head>
9
+ <body>
10
+ <canvas id="application-canvas"></canvas>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,65 @@
1
+ import {
2
+ AppBase,
3
+ AppOptions,
4
+ CameraComponentSystem,
5
+ Color,
6
+ ContainerHandler,
7
+ Entity,
8
+ FILLMODE_FILL_WINDOW,
9
+ LightComponentSystem,
10
+ RESOLUTION_AUTO,
11
+ RenderComponentSystem,
12
+ TextureHandler,
13
+ createGraphicsDevice
14
+ } from 'playcanvas';
15
+
16
+ import './style.css';
17
+
18
+ const canvas = document.getElementById('application-canvas') as HTMLCanvasElement;
19
+
20
+ const device = await createGraphicsDevice(canvas);
21
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
22
+
23
+ const createOptions = new AppOptions();
24
+ createOptions.graphicsDevice = device;
25
+ createOptions.componentSystems = [RenderComponentSystem, CameraComponentSystem, LightComponentSystem];
26
+ createOptions.resourceHandlers = [TextureHandler, ContainerHandler];
27
+
28
+ const app = new AppBase(canvas);
29
+ app.init(createOptions);
30
+ app.start();
31
+
32
+ // Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
33
+ app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
34
+ app.setCanvasResolution(RESOLUTION_AUTO);
35
+
36
+ // Ensure canvas is resized when window changes size
37
+ const resize = () => app.resizeCanvas();
38
+ window.addEventListener('resize', resize);
39
+ app.on('destroy', () => {
40
+ window.removeEventListener('resize', resize);
41
+ });
42
+
43
+ // Create box entity
44
+ const box = new Entity('cube');
45
+ box.addComponent('render', {
46
+ type: 'box'
47
+ });
48
+ app.root.addChild(box);
49
+
50
+ // Create camera entity
51
+ const camera = new Entity('camera');
52
+ camera.addComponent('camera', {
53
+ clearColor: new Color(0.5, 0.6, 0.9)
54
+ });
55
+ camera.setPosition(0, 0, 3);
56
+ app.root.addChild(camera);
57
+
58
+ // Create directional light entity
59
+ const light = new Entity('light');
60
+ light.addComponent('light');
61
+ light.setEulerAngles(45, 0, 0);
62
+ app.root.addChild(light);
63
+
64
+ // Rotate the box according to the delta time since the last frame
65
+ app.on('update', (dt: number) => box.rotate(10 * dt, 20 * dt, 30 * dt));
@@ -0,0 +1,8 @@
1
+ body {
2
+ margin: 0;
3
+ overflow: hidden;
4
+ }
5
+
6
+ canvas {
7
+ display: block;
8
+ }
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/png" href="/src/assets/playcanvas.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>PlayCanvas + TS</title>
8
+ </head>
9
+ <body>
10
+ <canvas id="application-canvas"></canvas>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,74 @@
1
+ import {
2
+ AppBase,
3
+ AppOptions,
4
+ Asset,
5
+ AssetListLoader,
6
+ CameraComponentSystem,
7
+ Entity,
8
+ FILLMODE_FILL_WINDOW,
9
+ GSplatComponentSystem,
10
+ GSplatHandler,
11
+ RESOLUTION_AUTO,
12
+ ScriptComponentSystem,
13
+ TextureHandler,
14
+ createGraphicsDevice
15
+ } from 'playcanvas';
16
+ import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
17
+
18
+ import './style.css';
19
+
20
+ // Hosted so the template stays small. Capture or edit your own splat at https://superspl.at, then
21
+ // drop the .sog or .ply in public/ and point this at it instead
22
+ const SPLAT_URL = 'https://developer.playcanvas.com/assets/toy-cat.sog';
23
+
24
+ const canvas = document.getElementById('application-canvas') as HTMLCanvasElement;
25
+
26
+ // Antialiasing is disabled - splat rendering is fragment bound, so MSAA costs a lot and adds little
27
+ const device = await createGraphicsDevice(canvas, { antialias: false });
28
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
29
+
30
+ const createOptions = new AppOptions();
31
+ createOptions.graphicsDevice = device;
32
+ createOptions.componentSystems = [CameraComponentSystem, GSplatComponentSystem, ScriptComponentSystem];
33
+
34
+ // TextureHandler is required as well as GSplatHandler - a .sog is a container whose splat data is
35
+ // stored as textures, and without it the splat loads but silently renders nothing
36
+ createOptions.resourceHandlers = [TextureHandler, GSplatHandler];
37
+
38
+ const app = new AppBase(canvas);
39
+ app.init(createOptions);
40
+
41
+ // Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
42
+ app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
43
+ app.setCanvasResolution(RESOLUTION_AUTO);
44
+
45
+ // Ensure canvas is resized when window changes size
46
+ const resize = () => app.resizeCanvas();
47
+ window.addEventListener('resize', resize);
48
+ app.on('destroy', () => {
49
+ window.removeEventListener('resize', resize);
50
+ });
51
+
52
+ // Load the splat before starting, so the first frame already has something in it
53
+ const splat = new Asset('splat', 'gsplat', { url: SPLAT_URL });
54
+
55
+ await new Promise<void>((resolve) => {
56
+ new AssetListLoader([splat], app.assets).load(() => resolve());
57
+ });
58
+
59
+ app.start();
60
+
61
+ // Splats are captured in their own space, so this one needs offsetting and flipping upright
62
+ const toyCat = new Entity('toy cat');
63
+ toyCat.addComponent('gsplat', { asset: splat });
64
+ toyCat.setPosition(0, -0.7, 0);
65
+ toyCat.setEulerAngles(0, 0, 180);
66
+ app.root.addChild(toyCat);
67
+
68
+ // Create camera entity with orbit controls
69
+ const camera = new Entity('camera');
70
+ camera.addComponent('camera');
71
+ camera.setPosition(0, 0, 2.5);
72
+ camera.addComponent('script');
73
+ camera.script?.create(CameraControls);
74
+ app.root.addChild(camera);
@@ -0,0 +1,8 @@
1
+ body {
2
+ margin: 0;
3
+ overflow: hidden;
4
+ }
5
+
6
+ canvas {
7
+ display: block;
8
+ }
@@ -1,18 +1,50 @@
1
1
  import colors from 'picocolors';
2
2
 
3
- const { cyan, yellow } = colors;
3
+ const { cyan, magenta, yellow } = colors;
4
4
 
5
5
  export const TEMPLATES = [
6
6
  {
7
- name: 'vanilla-ts',
8
- display: 'Vanilla',
9
- description: 'with PlayCanvas Engine',
7
+ name: 'engine',
8
+ display: 'Engine',
9
+ description: 'with the PlayCanvas Engine API directly',
10
10
  color: yellow
11
11
  },
12
12
  {
13
- name: 'react-ts',
13
+ name: 'react',
14
14
  display: 'React',
15
15
  description: 'with @playcanvas/react',
16
16
  color: cyan
17
+ },
18
+ {
19
+ name: 'web-components',
20
+ display: 'Web Components',
21
+ description: 'with @playcanvas/web-components',
22
+ color: magenta
23
+ }
24
+ ];
25
+
26
+ // Every template holds a `base` directory of shared tooling plus one directory per boilerplate,
27
+ // which is copied over the top. Assets a boilerplate needs in every template live once in
28
+ // `_shared/<boilerplate>` and are seeded in first. The first entry is the default.
29
+ export const BOILERPLATES = [
30
+ {
31
+ name: 'spinning-cube',
32
+ display: 'Spinning Cube',
33
+ description: 'a rotating cube and a light'
34
+ },
35
+ {
36
+ name: 'interactive-sphere',
37
+ display: 'Interactive Sphere',
38
+ description: 'orbit controls, a grid and pointer picking'
39
+ },
40
+ {
41
+ name: 'model-viewer',
42
+ display: 'Model Viewer',
43
+ description: 'a glTF model, orbit controls and a procedural sky'
44
+ },
45
+ {
46
+ name: 'splat-viewer',
47
+ display: 'Splat Viewer',
48
+ description: 'a Gaussian splat and orbit controls'
17
49
  }
18
50
  ];
@@ -9,13 +9,13 @@ Node.js 22.23.2 or later.
9
9
  ## Getting started
10
10
 
11
11
  ```bash
12
- npm create playcanvas@latest playcanvas-project -- --template react-ts
12
+ npm create playcanvas@latest playcanvas-project -- --template react
13
13
  cd playcanvas-project
14
14
  npm install
15
15
  npm run dev
16
16
  ```
17
17
 
18
- Open <http://localhost:5173>. Edit `src/Scene.tsx` to test hot module replacement.
18
+ Open <http://localhost:5173>. Edit the files under `src/` and save to see the scene update.
19
19
 
20
20
  ## Scripts
21
21
 
@@ -28,23 +28,6 @@ Open <http://localhost:5173>. Edit `src/Scene.tsx` to test hot module replacemen
28
28
  | `npm run fmt` | Check formatting |
29
29
  | `npm run typecheck` | Run TypeScript checks |
30
30
 
31
- ## Project structure
32
-
33
- ```text
34
- .
35
- ├── public/
36
- │ └── environment-map.png
37
- ├── src/
38
- │ ├── assets/
39
- │ ├── App.tsx
40
- │ ├── Scene.tsx
41
- │ └── main.tsx
42
- ├── eslint.config.mjs
43
- ├── index.html
44
- ├── tsconfig.json
45
- └── vite.config.ts
46
- ```
47
-
48
31
  Run `npm run build` to generate a deployable static site in `dist/`.
49
32
 
50
33
  ## Further reading
@@ -11,3 +11,9 @@ declare module 'playcanvas/scripts/esm/grid.mjs' {
11
11
 
12
12
  export const Grid: typeof Script & (new () => Script);
13
13
  }
14
+
15
+ declare module 'playcanvas/scripts/esm/sky/procedural-sky.mjs' {
16
+ import type { Script } from 'playcanvas';
17
+
18
+ export const ProceduralSky: typeof Script & (new () => Script);
19
+ }
@@ -9,7 +9,6 @@ import { useEffect, useState } from 'react';
9
9
  * The Scene renders a sphere with a grid and camera controls
10
10
  */
11
11
  function Scene({ onClick }: SceneProps) {
12
- // Track the hover state and set the color based on the hover state
13
12
  const [hovering, setHovering] = useState(false);
14
13
 
15
14
  // Set a material color based on the hover state
@@ -21,7 +20,7 @@ function Scene({ onClick }: SceneProps) {
21
20
  // Load the environment map
22
21
  const { asset: envMap } = useEnvAtlas('/environment-map.png');
23
22
 
24
- // change the mouse cursor based on the hover state
23
+ // Change the mouse cursor based on the hover state
25
24
  useEffect(() => {
26
25
  document.body.style.cursor = hovering ? 'pointer' : 'default';
27
26
 
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/png" href="/src/assets/playcanvas.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>PlayCanvas + React</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,43 @@
1
+ import { Application, Container, Entity } from '@playcanvas/react';
2
+ import { Camera, Script } from '@playcanvas/react/components';
3
+ import { useModel } from '@playcanvas/react/hooks';
4
+ import { TONEMAP_ACES2 } from 'playcanvas';
5
+ import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
6
+ import { ProceduralSky } from 'playcanvas/scripts/esm/sky/procedural-sky.mjs';
7
+
8
+ // Swap this for your own model - anything you drop in public/ is served from the site root
9
+ const MODEL_URL = '/playcanvas-cube.glb';
10
+
11
+ function Model() {
12
+ const { asset } = useModel(MODEL_URL);
13
+
14
+ // Don't render until the model is loaded
15
+ if (!asset) return null;
16
+
17
+ return <Container asset={asset} />;
18
+ }
19
+
20
+ function App() {
21
+ return (
22
+ <Application>
23
+ {/* The procedural sky is both the background and the image-based lighting, so no
24
+ light or environment map asset is needed. Its default luminance is scene-scale
25
+ bright, so turn it down to keep the model out of clipping */}
26
+ <Entity name="sky">
27
+ <Script script={ProceduralSky} luminance={0.2} />
28
+ </Entity>
29
+
30
+ {/* Create a camera entity with orbit controls. The sky is HDR, so it needs tone
31
+ mapping to bring it back into displayable range - without it the whole image blows
32
+ out to white */}
33
+ <Entity name="camera" position={[0, 0, 3]}>
34
+ <Camera toneMapping={TONEMAP_ACES2} />
35
+ <Script script={CameraControls} />
36
+ </Entity>
37
+
38
+ <Model />
39
+ </Application>
40
+ );
41
+ }
42
+
43
+ export default App;
@@ -0,0 +1,9 @@
1
+ body {
2
+ margin: 0;
3
+ overflow: hidden;
4
+ }
5
+
6
+ #root {
7
+ width: 100vw;
8
+ height: 100vh;
9
+ }
@@ -0,0 +1,11 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+
4
+ import './index.css';
5
+ import App from './App.tsx';
6
+
7
+ createRoot(document.getElementById('root')!).render(
8
+ <StrictMode>
9
+ <App />
10
+ </StrictMode>
11
+ );
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/png" href="/src/assets/playcanvas.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>PlayCanvas + React</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,36 @@
1
+ import { Application, Entity } from '@playcanvas/react';
2
+ import { Camera, Light, Render } from '@playcanvas/react/components';
3
+ import { useAppEvent } from '@playcanvas/react/hooks';
4
+ import type { Entity as PcEntity } from 'playcanvas';
5
+ import { useRef } from 'react';
6
+
7
+ function Scene() {
8
+ const cube = useRef<PcEntity>(null);
9
+
10
+ // Rotate the cube according to the delta time since the last frame
11
+ useAppEvent('update', (dt: number) => cube.current?.rotate(10 * dt, 20 * dt, 30 * dt));
12
+
13
+ return (
14
+ <>
15
+ <Entity name="camera" position={[0, 0, 3]}>
16
+ <Camera clearColor="#8099e6" />
17
+ </Entity>
18
+ <Entity name="light" rotation={[45, 0, 0]}>
19
+ <Light type="directional" />
20
+ </Entity>
21
+ <Entity name="cube" ref={cube}>
22
+ <Render type="box" />
23
+ </Entity>
24
+ </>
25
+ );
26
+ }
27
+
28
+ function App() {
29
+ return (
30
+ <Application>
31
+ <Scene />
32
+ </Application>
33
+ );
34
+ }
35
+
36
+ export default App;
@@ -0,0 +1,9 @@
1
+ body {
2
+ margin: 0;
3
+ overflow: hidden;
4
+ }
5
+
6
+ #root {
7
+ width: 100vw;
8
+ height: 100vh;
9
+ }
@@ -0,0 +1,11 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+
4
+ import './index.css';
5
+ import App from './App.tsx';
6
+
7
+ createRoot(document.getElementById('root')!).render(
8
+ <StrictMode>
9
+ <App />
10
+ </StrictMode>
11
+ );
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/png" href="/src/assets/playcanvas.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>PlayCanvas + React</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,39 @@
1
+ import { Application, Entity } from '@playcanvas/react';
2
+ import { Camera, GSplat, Script } from '@playcanvas/react/components';
3
+ import { useSplat } from '@playcanvas/react/hooks';
4
+ import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
5
+
6
+ // Hosted so the template stays small. Capture or edit your own splat at https://superspl.at, then
7
+ // drop the .sog or .ply in public/ and point this at it instead
8
+ const SPLAT_URL = 'https://developer.playcanvas.com/assets/toy-cat.sog';
9
+
10
+ function Splat() {
11
+ const { asset } = useSplat(SPLAT_URL);
12
+
13
+ // Don't render until the splat is loaded
14
+ if (!asset) return null;
15
+
16
+ // Splats are captured in their own space, so this one needs offsetting and flipping upright
17
+ return (
18
+ <Entity name="toy cat" position={[0, -0.7, 0]} rotation={[0, 0, 180]}>
19
+ <GSplat asset={asset} />
20
+ </Entity>
21
+ );
22
+ }
23
+
24
+ // Antialiasing is disabled - splat rendering is fragment bound, so MSAA costs a lot and adds little
25
+ function App() {
26
+ return (
27
+ <Application graphicsDeviceOptions={{ antialias: false }}>
28
+ {/* Create a camera entity with orbit controls */}
29
+ <Entity name="camera" position={[0, 0, 2.5]}>
30
+ <Camera />
31
+ <Script script={CameraControls} />
32
+ </Entity>
33
+
34
+ <Splat />
35
+ </Application>
36
+ );
37
+ }
38
+
39
+ export default App;
@@ -0,0 +1,9 @@
1
+ body {
2
+ margin: 0;
3
+ overflow: hidden;
4
+ }
5
+
6
+ #root {
7
+ width: 100vw;
8
+ height: 100vh;
9
+ }
@@ -0,0 +1,11 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+
4
+ import './index.css';
5
+ import App from './App.tsx';
6
+
7
+ createRoot(document.getElementById('root')!).render(
8
+ <StrictMode>
9
+ <App />
10
+ </StrictMode>
11
+ );
@@ -0,0 +1,66 @@
1
+ # PlayCanvas Web Components + TypeScript Starter
2
+
3
+ A Vite-powered `@playcanvas/web-components` project with TypeScript, ESLint and Prettier. The scene is
4
+ declared as HTML custom elements in `index.html`.
5
+
6
+ ## Prerequisites
7
+
8
+ Node.js 22.23.2 or later.
9
+
10
+ ## Getting started
11
+
12
+ ```bash
13
+ npm create playcanvas@latest playcanvas-project -- --template web-components
14
+ cd playcanvas-project
15
+ npm install
16
+ npm run dev
17
+ ```
18
+
19
+ Open <http://localhost:5173>. Edit `index.html` to change the scene.
20
+
21
+ ## Scripts
22
+
23
+ | Command | Description |
24
+ | ------------------- | --------------------------------- |
25
+ | `npm run dev` | Start the Vite development server |
26
+ | `npm run build` | Build for production |
27
+ | `npm run start` | Preview the production build |
28
+ | `npm run lint` | Run ESLint |
29
+ | `npm run fmt` | Check formatting |
30
+ | `npm run typecheck` | Run TypeScript checks |
31
+
32
+ Run `npm run build` to generate a deployable static site in `dist/`.
33
+
34
+ ## How it fits together
35
+
36
+ `index.html` holds the scene: `<pc-app>` wraps a `<pc-scene>` of `<pc-entity>` elements, each with
37
+ components such as `<pc-camera>`, `<pc-light>` and `<pc-render>` attached. Entities are real DOM elements,
38
+ so `document.querySelector` reaches them and `setAttribute` changes them.
39
+
40
+ `src/main.ts` covers the two things HTML alone cannot do:
41
+
42
+ - **Attaching engine scripts.** `<pc-script name="...">` resolves scripts that `<pc-asset>` fetched at
43
+ runtime. Scripts imported through the bundler — like `CameraControls` and `Grid` here — are attached
44
+ through the entity instead, reached via `whenReady()` and the element's `.entity` property.
45
+ - **Responding to pointer input.** `<pc-app>` picks whatever is under the pointer and dispatches an
46
+ ordinary `PointerEvent` on the `<pc-entity>` it hit, so `addEventListener('pointerup', ...)` works on a
47
+ 3D entity exactly as it would on a `<button>`. Events are only generated for entities that have a
48
+ listener.
49
+
50
+ ## Editor support
51
+
52
+ The package ships a Custom Elements Manifest, which gives tag and attribute completion in `index.html`.
53
+ JetBrains IDEs pick it up automatically. For VS Code, add to `.vscode/settings.json`:
54
+
55
+ ```json
56
+ {
57
+ "html.customData": ["./node_modules/@playcanvas/web-components/dist/vscode.html-custom-data.json"]
58
+ }
59
+ ```
60
+
61
+ ## Further reading
62
+
63
+ - [PlayCanvas Web Components manual](https://developer.playcanvas.com/user-manual/web-components/)
64
+ - [Web Components examples](https://playcanvas.github.io/web-components/examples)
65
+ - [Vite documentation](https://vite.dev/)
66
+ - [TypeScript documentation](https://www.typescriptlang.org/docs/)
@@ -0,0 +1,24 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
@@ -0,0 +1,3 @@
1
+ import typescriptConfig from '@playcanvas/eslint-config/typescript';
2
+
3
+ export default typescriptConfig;