create-playcanvas 0.3.0 → 0.5.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 (114) hide show
  1. package/README.md +42 -25
  2. package/dist/index.mjs +49 -46
  3. package/package.json +3 -1
  4. package/skills/add-effects/SKILL.md +81 -0
  5. package/skills/apply-conventions/SKILL.md +73 -0
  6. package/skills/assemble-scene/SKILL.md +53 -0
  7. package/skills/assemble-scene/references/direct-engine.md +16 -0
  8. package/skills/assemble-scene/references/react.md +24 -0
  9. package/skills/assemble-scene/references/web-components.md +19 -0
  10. package/skills/build-app/SKILL.md +41 -0
  11. package/skills/build-app/references/direct-engine.md +12 -0
  12. package/skills/build-app/references/react.md +20 -0
  13. package/skills/build-app/references/web-components.md +55 -0
  14. package/skills/build-hud/SKILL.md +36 -0
  15. package/skills/calibrate-model/SKILL.md +65 -0
  16. package/skills/calibrate-model/references/direct-engine.md +22 -0
  17. package/skills/calibrate-model/references/react.md +27 -0
  18. package/skills/calibrate-model/references/web-components.md +19 -0
  19. package/skills/configure-animation/SKILL.md +50 -0
  20. package/skills/configure-animation/references/direct-engine.md +32 -0
  21. package/skills/configure-animation/references/react.md +32 -0
  22. package/skills/configure-animation/references/web-components.md +13 -0
  23. package/skills/find-examples/SKILL.md +79 -0
  24. package/skills/find-examples/references/direct-engine.md +15 -0
  25. package/skills/find-examples/references/react.md +17 -0
  26. package/skills/find-examples/references/web-components.md +17 -0
  27. package/skills/inspect-glb/SKILL.md +72 -0
  28. package/skills/inspect-glb/scripts/inspect.mjs +239 -0
  29. package/skills/light-scene/SKILL.md +84 -0
  30. package/skills/manage-game-state/SKILL.md +48 -0
  31. package/skills/reuse-scripts/SKILL.md +59 -0
  32. package/skills/reuse-scripts/references/direct-engine.md +41 -0
  33. package/skills/reuse-scripts/references/react.md +28 -0
  34. package/skills/reuse-scripts/references/web-components.md +27 -0
  35. package/templates/_shared/model-viewer/public/playcanvas-cube.glb +0 -0
  36. package/templates/engine/base/.prettierignore +2 -0
  37. package/templates/{vanilla-ts → engine/base}/README.md +7 -19
  38. package/templates/engine/base/eslint.config.mjs +3 -0
  39. package/templates/{vanilla-ts → engine/base}/src/vite-env.d.ts +6 -0
  40. package/templates/{vanilla-ts → engine/interactive-sphere}/src/main.ts +1 -1
  41. package/templates/{vanilla-ts → engine/interactive-sphere}/src/style.css +1 -1
  42. package/templates/{vanilla-ts → engine/interactive-sphere}/src/utils.ts +0 -1
  43. package/templates/engine/model-viewer/index.html +13 -0
  44. package/templates/engine/model-viewer/src/main.ts +81 -0
  45. package/templates/engine/model-viewer/src/style.css +8 -0
  46. package/templates/engine/spinning-cube/index.html +13 -0
  47. package/templates/engine/spinning-cube/src/main.ts +65 -0
  48. package/templates/engine/spinning-cube/src/style.css +8 -0
  49. package/templates/engine/splat-viewer/index.html +13 -0
  50. package/templates/engine/splat-viewer/src/main.ts +74 -0
  51. package/templates/engine/splat-viewer/src/style.css +8 -0
  52. package/templates/index.ts +37 -5
  53. package/templates/react/base/.prettierignore +2 -0
  54. package/templates/{react-ts → react/base}/README.md +6 -19
  55. package/templates/{react-ts → react/base}/eslint.config.mjs +1 -0
  56. package/templates/{react-ts → react/base}/src/vite-env.d.ts +6 -0
  57. package/templates/{react-ts → react/interactive-sphere}/src/Scene.tsx +1 -2
  58. package/templates/react/model-viewer/index.html +13 -0
  59. package/templates/react/model-viewer/src/App.tsx +43 -0
  60. package/templates/react/model-viewer/src/index.css +9 -0
  61. package/templates/react/model-viewer/src/main.tsx +11 -0
  62. package/templates/react/spinning-cube/index.html +13 -0
  63. package/templates/react/spinning-cube/src/App.tsx +36 -0
  64. package/templates/react/spinning-cube/src/index.css +9 -0
  65. package/templates/react/spinning-cube/src/main.tsx +11 -0
  66. package/templates/react/splat-viewer/index.html +13 -0
  67. package/templates/react/splat-viewer/src/App.tsx +39 -0
  68. package/templates/react/splat-viewer/src/index.css +9 -0
  69. package/templates/react/splat-viewer/src/main.tsx +11 -0
  70. package/templates/web-components/base/.prettierignore +2 -0
  71. package/templates/web-components/base/README.md +70 -0
  72. package/templates/web-components/base/_gitignore +24 -0
  73. package/templates/web-components/base/eslint.config.mjs +3 -0
  74. package/templates/web-components/base/package.json +31 -0
  75. package/templates/web-components/base/prettier.config.mjs +1 -0
  76. package/templates/web-components/base/src/assets/playcanvas.png +0 -0
  77. package/templates/web-components/base/src/vite-env.d.ts +19 -0
  78. package/templates/web-components/base/tsconfig.json +25 -0
  79. package/templates/web-components/interactive-sphere/index.html +53 -0
  80. package/templates/web-components/interactive-sphere/src/assets/typescript.svg +1 -0
  81. package/templates/web-components/interactive-sphere/src/main.ts +76 -0
  82. package/templates/web-components/interactive-sphere/src/style.css +111 -0
  83. package/templates/web-components/model-viewer/index.html +34 -0
  84. package/templates/web-components/model-viewer/src/main.ts +26 -0
  85. package/templates/web-components/model-viewer/src/style.css +8 -0
  86. package/templates/web-components/spinning-cube/index.html +29 -0
  87. package/templates/web-components/spinning-cube/src/main.ts +20 -0
  88. package/templates/web-components/spinning-cube/src/style.css +8 -0
  89. package/templates/web-components/splat-viewer/index.html +31 -0
  90. package/templates/web-components/splat-viewer/src/main.ts +14 -0
  91. package/templates/web-components/splat-viewer/src/style.css +8 -0
  92. package/templates/vanilla-ts/eslint.config.mjs +0 -3
  93. package/templates/vanilla-ts/public/environment-map.png +0 -0
  94. /package/templates/{react-ts → _shared/interactive-sphere}/public/environment-map.png +0 -0
  95. /package/templates/{react-ts → engine/base}/_gitignore +0 -0
  96. /package/templates/{vanilla-ts → engine/base}/package.json +0 -0
  97. /package/templates/{react-ts → engine/base}/prettier.config.mjs +0 -0
  98. /package/templates/{react-ts → engine/base}/src/assets/playcanvas.png +0 -0
  99. /package/templates/{vanilla-ts → engine/base}/tsconfig.json +0 -0
  100. /package/templates/{vanilla-ts → engine/interactive-sphere}/index.html +0 -0
  101. /package/templates/{vanilla-ts → engine/interactive-sphere}/src/App.ts +0 -0
  102. /package/templates/{vanilla-ts → engine/interactive-sphere}/src/assets/typescript.svg +0 -0
  103. /package/templates/{vanilla-ts → react/base}/_gitignore +0 -0
  104. /package/templates/{react-ts → react/base}/package.json +0 -0
  105. /package/templates/{vanilla-ts → react/base}/prettier.config.mjs +0 -0
  106. /package/templates/{vanilla-ts → react/base}/src/assets/playcanvas.png +0 -0
  107. /package/templates/{react-ts → react/base}/tsconfig.json +0 -0
  108. /package/templates/{react-ts → react/base}/vite.config.ts +0 -0
  109. /package/templates/{react-ts → react/interactive-sphere}/index.html +0 -0
  110. /package/templates/{react-ts → react/interactive-sphere}/src/App.css +0 -0
  111. /package/templates/{react-ts → react/interactive-sphere}/src/App.tsx +0 -0
  112. /package/templates/{react-ts → react/interactive-sphere}/src/assets/react.svg +0 -0
  113. /package/templates/{react-ts → react/interactive-sphere}/src/index.css +0 -0
  114. /package/templates/{react-ts → react/interactive-sphere}/src/main.tsx +0 -0
@@ -1,4 +1,4 @@
1
- # PlayCanvas + TypeScript Starter
1
+ # PlayCanvas Engine + TypeScript Starter
2
2
 
3
3
  A Vite-powered PlayCanvas Engine project with TypeScript, hot module replacement, ESLint and Prettier.
4
4
 
@@ -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 vanilla-ts
12
+ npm create playcanvas@latest playcanvas-project -- --template engine
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/App.ts` 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,24 +28,12 @@ Open <http://localhost:5173>. Edit `src/App.ts` to test hot module replacement.
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.ts
40
- │ ├── main.ts
41
- │ └── style.css
42
- ├── eslint.config.mjs
43
- ├── index.html
44
- └── tsconfig.json
45
- ```
46
-
47
31
  Run `npm run build` to generate a deployable static site in `dist/`.
48
32
 
33
+ ## Agent skills
34
+
35
+ This project includes [`@playcanvas/skills`](https://github.com/playcanvas/skills) under `.claude/skills/` and `.agents/skills/`, so Claude Code, Codex and Cursor pick up PlayCanvas-specific workflows automatically.
36
+
49
37
  ## Further reading
50
38
 
51
39
  - [PlayCanvas Engine manual](https://developer.playcanvas.com/user-manual/engine/)
@@ -0,0 +1,3 @@
1
+ import typescriptConfig from '@playcanvas/eslint-config/typescript';
2
+
3
+ export default [{ ignores: ['.claude', '.agents'] }, ...typescriptConfig];
@@ -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
+ }
@@ -16,7 +16,7 @@ document.querySelector<HTMLDivElement>('#root')!.innerHTML = `
16
16
  <img src="${playcanvasLogo}" class="playcanvas-logo logo" alt="PlayCanvas logo" />
17
17
  </a>
18
18
  <a href="https://www.typescriptlang.org/" target="_blank">
19
- <img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
19
+ <img src="${typescriptLogo}" class="logo typescript" alt="TypeScript logo" />
20
20
  </a>
21
21
  </header>
22
22
  </div>
@@ -59,7 +59,7 @@ h1 {
59
59
  filter: drop-shadow(0 0 2em #646cffaa);
60
60
  }
61
61
 
62
- .logo.vanilla:hover {
62
+ .logo.typescript:hover {
63
63
  filter: drop-shadow(0 0 2em #3178c6aa);
64
64
  }
65
65
 
@@ -5,7 +5,6 @@
5
5
  * @returns A throttled function
6
6
  */
7
7
  export const throttle = <T extends unknown[]>(fn: (...args: T) => void, interval = 100) => {
8
- // interval in ms
9
8
  let last = 0;
10
9
  return (...args: T) => {
11
10
  const now = Date.now();
@@ -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,81 @@
1
+ import type { ContainerResource } from 'playcanvas';
2
+ import {
3
+ AppBase,
4
+ AppOptions,
5
+ Asset,
6
+ AssetListLoader,
7
+ CameraComponentSystem,
8
+ ContainerHandler,
9
+ Entity,
10
+ FILLMODE_FILL_WINDOW,
11
+ RESOLUTION_AUTO,
12
+ RenderComponentSystem,
13
+ ScriptComponentSystem,
14
+ TONEMAP_ACES2,
15
+ TextureHandler,
16
+ createGraphicsDevice
17
+ } from 'playcanvas';
18
+ import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
19
+ import { ProceduralSky } from 'playcanvas/scripts/esm/sky/procedural-sky.mjs';
20
+
21
+ import './style.css';
22
+
23
+ // Swap this for your own model - anything you drop in public/ is served from the site root
24
+ const MODEL_URL = '/playcanvas-cube.glb';
25
+
26
+ const canvas = document.getElementById('application-canvas') as HTMLCanvasElement;
27
+
28
+ const device = await createGraphicsDevice(canvas);
29
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
30
+
31
+ const createOptions = new AppOptions();
32
+ createOptions.graphicsDevice = device;
33
+ createOptions.componentSystems = [RenderComponentSystem, CameraComponentSystem, ScriptComponentSystem];
34
+ createOptions.resourceHandlers = [TextureHandler, ContainerHandler];
35
+
36
+ const app = new AppBase(canvas);
37
+ app.init(createOptions);
38
+
39
+ // Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
40
+ app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
41
+ app.setCanvasResolution(RESOLUTION_AUTO);
42
+
43
+ // Ensure canvas is resized when window changes size
44
+ const resize = () => app.resizeCanvas();
45
+ window.addEventListener('resize', resize);
46
+ app.on('destroy', () => {
47
+ window.removeEventListener('resize', resize);
48
+ });
49
+
50
+ // Load the model before starting, so the first frame already has something in it
51
+ const model = new Asset('model', 'container', { url: MODEL_URL });
52
+
53
+ await new Promise<void>((resolve) => {
54
+ new AssetListLoader([model], app.assets).load(() => resolve());
55
+ });
56
+
57
+ app.start();
58
+
59
+ // A container asset holds the whole glTF scene, so instantiate it to get an entity hierarchy
60
+ app.root.addChild((model.resource as ContainerResource).instantiateRenderEntity());
61
+
62
+ // The procedural sky is both the background and the image-based lighting, so no light or
63
+ // environment map asset is needed. Its default luminance is scene-scale bright, so turn it down to
64
+ // keep the model out of clipping
65
+ const sky = new Entity('sky');
66
+ sky.addComponent('script');
67
+ sky.script?.create(ProceduralSky, {
68
+ properties: { luminance: 0.2 }
69
+ });
70
+ app.root.addChild(sky);
71
+
72
+ // Create camera entity with orbit controls. The sky is HDR, so it needs tone mapping to bring it
73
+ // back into displayable range - without it the whole image blows out to white
74
+ const camera = new Entity('camera');
75
+ camera.addComponent('camera', {
76
+ toneMapping: TONEMAP_ACES2
77
+ });
78
+ camera.setPosition(0, 0, 3);
79
+ camera.addComponent('script');
80
+ camera.script?.create(CameraControls);
81
+ app.root.addChild(camera);
@@ -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,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
  ];
@@ -0,0 +1,2 @@
1
+ .claude
2
+ .agents
@@ -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,25 +28,12 @@ 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
 
33
+ ## Agent skills
34
+
35
+ This project includes [`@playcanvas/skills`](https://github.com/playcanvas/skills) under `.claude/skills/` and `.agents/skills/`, so Claude Code, Codex and Cursor pick up PlayCanvas-specific workflows automatically.
36
+
50
37
  ## Further reading
51
38
 
52
39
  - [PlayCanvas React manual](https://developer.playcanvas.com/user-manual/react/)
@@ -2,6 +2,7 @@ import reactConfig from '@playcanvas/eslint-config/react';
2
2
  import typescriptConfig from '@playcanvas/eslint-config/typescript';
3
3
 
4
4
  export default [
5
+ { ignores: ['.claude', '.agents'] },
5
6
  ...typescriptConfig,
6
7
  ...reactConfig,
7
8
  {
@@ -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;