create-playcanvas 0.15.0 → 0.16.1

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 (43) hide show
  1. package/README.md +69 -24
  2. package/dist/index.mjs +11 -11
  3. package/package.json +1 -1
  4. package/templates/_shared/scene-editor/AGENTS.md +8 -0
  5. package/templates/_shared/scene-editor/README.md +15 -2
  6. package/templates/_shared/scene-editor/src/scene-editor.ts +186 -36
  7. package/templates/engine/ar-placement/index.html +20 -0
  8. package/templates/engine/ar-placement/src/main.ts +22 -12
  9. package/templates/engine/base/AGENTS.md +1 -0
  10. package/templates/engine/base/README.md +1 -1
  11. package/templates/engine/first-person-controller/index.html +19 -0
  12. package/templates/engine/first-person-controller/src/main.ts +16 -11
  13. package/templates/engine/product-configurator/index.html +30 -0
  14. package/templates/engine/product-configurator/src/main.ts +33 -32
  15. package/templates/engine/scene-editor/index.html +33 -0
  16. package/templates/engine/scene-editor/src/main.ts +56 -47
  17. package/templates/engine/sprite-game/index.html +19 -0
  18. package/templates/engine/sprite-game/src/main.ts +19 -11
  19. package/templates/engine/third-person-controller/index.html +19 -0
  20. package/templates/engine/third-person-controller/src/main.ts +16 -11
  21. package/templates/engine/vr-starter/index.html +20 -0
  22. package/templates/engine/vr-starter/src/main.ts +22 -12
  23. package/templates/index.ts +1 -14
  24. package/templates/react/base/AGENTS.md +1 -0
  25. package/templates/react/scene-editor/src/App.tsx +73 -40
  26. package/templates/web-components/base/AGENTS.md +1 -0
  27. package/templates/web-components/scene-editor/index.html +32 -25
  28. package/templates/web-components/scene-editor/src/main.ts +33 -11
  29. package/templates/_shared/interactive-sphere/README.md +0 -7
  30. package/templates/_shared/interactive-sphere/public/environment-map.png +0 -0
  31. package/templates/_shared/interactive-sphere/src/counter.css +0 -19
  32. package/templates/_shared/physics-playground/README.md +0 -7
  33. package/templates/engine/interactive-sphere/src/App.ts +0 -182
  34. package/templates/engine/interactive-sphere/src/main.ts +0 -16
  35. package/templates/engine/interactive-sphere/src/utils.ts +0 -16
  36. package/templates/engine/physics-playground/src/main.ts +0 -131
  37. package/templates/react/interactive-sphere/src/App.tsx +0 -21
  38. package/templates/react/interactive-sphere/src/Scene.tsx +0 -70
  39. package/templates/react/physics-playground/src/App.tsx +0 -85
  40. package/templates/web-components/interactive-sphere/index.html +0 -36
  41. package/templates/web-components/interactive-sphere/src/main.ts +0 -76
  42. package/templates/web-components/physics-playground/index.html +0 -46
  43. package/templates/web-components/physics-playground/src/main.ts +0 -32
@@ -17,23 +17,35 @@ import {
17
17
  import './starter.css';
18
18
  import { setupVr } from './vr';
19
19
 
20
- document.body.insertAdjacentHTML(
21
- 'beforeend',
22
- '<div class="hud"><section class="panel"><h1>VR Starter</h1><p id="xr-status">Checking VR support…</p><div class="controls"><button id="xr-button" disabled>Enter VR</button></div></section></div>'
23
- );
24
-
25
20
  const canvas = document.getElementById('application-canvas') as HTMLCanvasElement;
21
+
26
22
  const device = await createGraphicsDevice(canvas);
27
- const options = new AppOptions();
28
- options.graphicsDevice = device;
29
- options.componentSystems = [RenderComponentSystem, CameraComponentSystem, LightComponentSystem, ScriptComponentSystem];
30
- options.xr = XrManager;
23
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
24
+
25
+ const createOptions = new AppOptions();
26
+ createOptions.graphicsDevice = device;
27
+ createOptions.componentSystems = [
28
+ RenderComponentSystem,
29
+ CameraComponentSystem,
30
+ LightComponentSystem,
31
+ ScriptComponentSystem
32
+ ];
33
+ createOptions.xr = XrManager;
31
34
 
32
35
  const app = new AppBase(canvas);
33
- app.init(options);
36
+ app.init(createOptions);
34
37
  app.start();
38
+
39
+ // Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
35
40
  app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
36
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
+ });
37
49
  app.scene.ambientLight = new Color(0.32, 0.34, 0.38);
38
50
 
39
51
  const material = (color: Color) => {
@@ -97,5 +109,3 @@ light.addComponent('light', {
97
109
  });
98
110
  light.setEulerAngles(45, 30, 0);
99
111
  app.root.addChild(light);
100
-
101
- window.addEventListener('resize', () => app.resizeCanvas());
@@ -41,12 +41,6 @@ export const STARTERS = [
41
41
  category: 'Basics',
42
42
  description: 'a rotating cube and a light'
43
43
  },
44
- {
45
- name: 'interactive-sphere',
46
- display: 'Interactive Sphere',
47
- category: 'Basics',
48
- description: 'orbit controls, a grid and pointer picking'
49
- },
50
44
  {
51
45
  name: 'model-viewer',
52
46
  display: 'Model Viewer',
@@ -65,13 +59,6 @@ export const STARTERS = [
65
59
  category: 'Viewers',
66
60
  description: 'product variants, materials and orbit controls'
67
61
  },
68
- {
69
- name: 'physics-playground',
70
- display: 'Physics Playground',
71
- category: 'Games',
72
- description: 'rigid bodies, collisions, spawning and reset',
73
- features: ['physics']
74
- },
75
62
  {
76
63
  name: 'first-person-controller',
77
64
  display: 'First-Person Controller',
@@ -105,7 +92,7 @@ export const STARTERS = [
105
92
  name: 'scene-editor',
106
93
  display: 'Scene Editor',
107
94
  category: 'Tools',
108
- description: 'selection and translate, rotate and scale gizmos'
95
+ description: 'click to select, gizmos and a scene saved in the browser'
109
96
  },
110
97
  {
111
98
  name: 'vr-starter',
@@ -30,6 +30,7 @@ A PlayCanvas app in TypeScript and React, built with Vite. The scene is declared
30
30
  - Builds use relative URLs (`base: './'` in `vite.config.ts`), so they run from any folder. Load files in `public/` with relative paths such as `./model.glb`, not `/model.glb`.
31
31
  - The engine ships ready-made scripts, such as camera controls and character controllers, under `playcanvas/scripts/esm/`. Use one before writing the same behavior yourself.
32
32
  - `node_modules/playcanvas/build/playcanvas.d.ts` documents the installed engine's API.
33
+ - For guides, start from the [PlayCanvas React guides index](https://developer.playcanvas.com/user-manual/react/llms.txt), which links a Markdown version of every page.
33
34
  - TypeScript is strict, and unused locals and parameters are errors.
34
35
 
35
36
  ## Checking your work
@@ -1,79 +1,112 @@
1
1
  import { Application, Entity } from '@playcanvas/react';
2
- import { Camera, Light, Render, Script } from '@playcanvas/react/components';
3
- import { useApp, useMaterial } from '@playcanvas/react/hooks';
2
+ import { Camera, Light, Script } from '@playcanvas/react/components';
3
+ import { useApp, useAppEvent } from '@playcanvas/react/hooks';
4
4
  import type { Entity as PcEntity } from 'playcanvas';
5
+ import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
5
6
  import { Grid } from 'playcanvas/scripts/esm/grid.mjs';
6
- import { useEffect, useRef } from 'react';
7
+ import { useCallback, useEffect, useRef, useState } from 'react';
8
+ import type { RefObject } from 'react';
7
9
 
8
10
  import { SceneEditor } from './scene-editor';
11
+ import type { EditorEvents, Mode, Shape } from './scene-editor';
9
12
  import './starter.css';
10
13
 
11
- function Scene() {
14
+ const MODES: { mode: Mode; label: string }[] = [
15
+ { mode: 'translate', label: 'Move' },
16
+ { mode: 'rotate', label: 'Rotate' },
17
+ { mode: 'scale', label: 'Scale' }
18
+ ];
19
+ const SHAPES: Shape[] = ['box', 'sphere', 'cone'];
20
+
21
+ function Scene({
22
+ editor,
23
+ mode,
24
+ onSelect
25
+ }: {
26
+ editor: RefObject<SceneEditor | null>;
27
+ mode: Mode;
28
+ onSelect: EditorEvents['editor:select'];
29
+ }) {
12
30
  const app = useApp();
13
- const host = useRef<PcEntity>(null);
14
- const camera = useRef<PcEntity>(null);
15
- const cube = useRef<PcEntity>(null);
16
- const sphere = useRef<PcEntity>(null);
17
- const coral = useMaterial({ diffuse: '#ff5145' });
18
- const cyan = useMaterial({ diffuse: '#24addf' });
31
+
32
+ // the editor needs the camera's entity, which exists once the camera has rendered
33
+ const [camera, setCamera] = useState<PcEntity | null>(null);
34
+
35
+ useAppEvent<EditorEvents>('editor:select', onSelect);
19
36
 
20
37
  useEffect(() => {
21
- const node = host.current;
22
- const layer = app.scene.layers.getLayerByName('World');
23
- if (!node || !camera.current || !cube.current || !sphere.current || !layer) return;
24
- if (!node.script) node.addComponent('script');
25
- node.script!.create(SceneEditor, {
26
- properties: { camera: camera.current, targets: [cube.current, sphere.current], layer }
27
- });
28
- return () => {
29
- node.script?.destroy('sceneEditor');
30
- };
38
+ // render at up to twice the CSS resolution: sharp on a phone, without the cost of 3x
39
+ // eslint-disable-next-line react-hooks/immutability -- the device belongs to the engine, not React
40
+ app.graphicsDevice.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
31
41
  }, [app]);
32
42
 
33
43
  return (
34
44
  <>
35
- <Entity name="camera" ref={camera} position={[4, 3.2, 5]} rotation={[-18, 39, 0]}>
36
- <Camera clearColor="#0a0f17" />
37
- </Entity>
38
45
  <Entity name="light" rotation={[45, 30, 0]}>
39
- <Light type="directional" intensity={2.5} castShadows shadowBias={0.2} normalOffsetBias={0.05} />
46
+ <Light type="directional" intensity={2.5} />
47
+ </Entity>
48
+ {/* A weaker light from behind keeps the sides the first can't reach from going black */}
49
+ <Entity name="fill" rotation={[-25, -140, 0]}>
50
+ <Light type="directional" intensity={0.8} />
40
51
  </Entity>
41
52
  <Entity name="grid" scale={[12, 12, 12]}>
42
53
  <Script script={Grid} />
43
54
  </Entity>
44
- <Entity name="cube" ref={cube} position={[-1.2, 0.5, 0]}>
45
- <Render type="box" material={coral} />
46
- </Entity>
47
- <Entity name="sphere" ref={sphere} position={[1.2, 0.5, 0]}>
48
- <Render type="sphere" material={cyan} />
55
+ {/* Create a camera entity with orbit controls */}
56
+ <Entity name="camera" ref={setCamera} position={[4, 3.2, 5]} rotation={[-23, 39, 0]}>
57
+ <Camera clearColor="#0a0f17" />
58
+ <Script script={CameraControls} sceneSize={5} />
49
59
  </Entity>
50
- <Entity name="editor" ref={host} />
60
+ {/* The editor creates the objects under its own entity, from the scene saved in this browser */}
61
+ {camera && (
62
+ <Entity name="editor">
63
+ <Script ref={editor} script={SceneEditor} camera={camera} mode={mode} />
64
+ </Entity>
65
+ )}
51
66
  </>
52
67
  );
53
68
  }
54
69
 
55
70
  function App() {
71
+ // the HUD's buttons call the editor script through this
72
+ const editor = useRef<SceneEditor>(null);
73
+ const [mode, setMode] = useState<Mode>('translate');
74
+ const [selected, setSelected] = useState(false);
75
+
76
+ const onSelect = useCallback((entity: PcEntity | null) => setSelected(!!entity), []);
77
+
56
78
  return (
57
79
  <>
58
80
  <Application>
59
- <Scene />
81
+ <Scene editor={editor} mode={mode} onSelect={onSelect} />
60
82
  </Application>
61
83
  <div className="hud">
62
84
  <section className="panel">
63
85
  <h1>Scene Editor</h1>
64
- <p>Select an object, then translate, rotate or scale it.</p>
86
+ <p>Click an object to select it, then drag its gizmo. Drag anywhere else to orbit.</p>
65
87
  <div className="controls">
66
- <button data-target="0" aria-pressed="true">
67
- Cube
68
- </button>
69
- <button data-target="1">Sphere</button>
88
+ {MODES.map((item) => (
89
+ <button
90
+ key={item.mode}
91
+ aria-pressed={mode === item.mode}
92
+ onClick={() => setMode(item.mode)}
93
+ >
94
+ {item.label}
95
+ </button>
96
+ ))}
97
+ </div>
98
+ <div className="controls">
99
+ {SHAPES.map((shape) => (
100
+ <button key={shape} onClick={() => editor.current?.add(shape)}>
101
+ Add {shape}
102
+ </button>
103
+ ))}
70
104
  </div>
71
105
  <div className="controls">
72
- <button data-mode="translate" aria-pressed="true">
73
- Move
106
+ <button disabled={!selected} onClick={() => editor.current?.remove()}>
107
+ Delete
74
108
  </button>
75
- <button data-mode="rotate">Rotate</button>
76
- <button data-mode="scale">Scale</button>
109
+ <button onClick={() => editor.current?.reset()}>Reset</button>
77
110
  </div>
78
111
  </section>
79
112
  </div>
@@ -28,6 +28,7 @@ A PlayCanvas app in TypeScript, built with Vite. The scene is declared in HTML w
28
28
  - Builds use relative URLs (`base: './'` in `vite.config.ts`), so they run from any folder. Load files in `public/` with relative paths such as `./model.glb`, not `/model.glb`.
29
29
  - The engine ships ready-made scripts, such as camera controls and character controllers, under `playcanvas/scripts/esm/`. Use one before writing the same behavior yourself.
30
30
  - `node_modules/playcanvas/build/playcanvas.d.ts` documents the installed engine's API.
31
+ - For guides, start from the [Web Components guides index](https://developer.playcanvas.com/user-manual/web-components/llms.txt), which links a Markdown version of every page.
31
32
  - TypeScript is strict, and unused locals and parameters are errors.
32
33
 
33
34
  ## Checking your work
@@ -7,45 +7,52 @@
7
7
  <title>Scene Editor</title>
8
8
  </head>
9
9
  <body>
10
- <pc-app>
11
- <pc-material id="coral" diffuse="#ff5145"></pc-material>
12
- <pc-material id="cyan" diffuse="#24addf"></pc-material>
10
+ <pc-app max-pixel-ratio="2">
13
11
  <pc-scene>
14
- <pc-entity name="camera" position="4 3.2 5" rotation="-18 39 0"
15
- ><pc-camera clear-color="#0a0f17"></pc-camera
16
- ></pc-entity>
17
- <pc-entity name="light" rotation="45 30 0"
18
- ><pc-light
19
- type="directional"
20
- intensity="2.5"
21
- cast-shadows
22
- shadow-bias="0.2"
23
- normal-offset-bias="0.05"
24
- ></pc-light
25
- ></pc-entity>
12
+ <pc-entity name="light" rotation="45 30 0">
13
+ <pc-light type="directional" intensity="2.5"></pc-light>
14
+ </pc-entity>
15
+
16
+ <!-- A weaker light from behind keeps the sides the first can't reach from going black -->
17
+ <pc-entity name="fill" rotation="-25 -140 0">
18
+ <pc-light type="directional" intensity="0.8"></pc-light>
19
+ </pc-entity>
20
+
21
+ <!-- The grid script is attached in src/main.ts -->
26
22
  <pc-entity name="grid" scale="12 12 12"></pc-entity>
27
- <pc-entity name="cube" position="-1.2 0.5 0"
28
- ><pc-render type="box" material="coral"></pc-render
29
- ></pc-entity>
30
- <pc-entity name="sphere" position="1.2 0.5 0"
31
- ><pc-render type="sphere" material="cyan"></pc-render
32
- ></pc-entity>
23
+
24
+ <!-- Camera. Orbit controls are attached in src/main.ts -->
25
+ <pc-entity name="camera" position="4 3.2 5" rotation="-23 39 0">
26
+ <pc-camera clear-color="#0a0f17"></pc-camera>
27
+ </pc-entity>
28
+
29
+ <!-- The editor script is attached in src/main.ts. It creates the objects under this
30
+ entity, from the scene saved in this browser -->
33
31
  <pc-entity name="editor"></pc-entity>
34
32
  </pc-scene>
35
33
  </pc-app>
34
+
36
35
  <div class="hud">
37
36
  <section class="panel">
38
37
  <h1>Scene Editor</h1>
39
- <p>Select an object, then translate, rotate or scale it.</p>
38
+ <p>Click an object to select it, then drag its gizmo. Drag anywhere else to orbit.</p>
40
39
  <div class="controls">
41
- <button data-target="0" aria-pressed="true">Cube</button><button data-target="1">Sphere</button>
40
+ <button data-mode="translate" aria-pressed="true">Move</button>
41
+ <button data-mode="rotate" aria-pressed="false">Rotate</button>
42
+ <button data-mode="scale" aria-pressed="false">Scale</button>
42
43
  </div>
43
44
  <div class="controls">
44
- <button data-mode="translate" aria-pressed="true">Move</button
45
- ><button data-mode="rotate">Rotate</button><button data-mode="scale">Scale</button>
45
+ <button data-add="box">Add box</button>
46
+ <button data-add="sphere">Add sphere</button>
47
+ <button data-add="cone">Add cone</button>
48
+ </div>
49
+ <div class="controls">
50
+ <button id="delete" disabled>Delete</button>
51
+ <button id="reset">Reset</button>
46
52
  </div>
47
53
  </section>
48
54
  </div>
55
+
49
56
  <script type="module" src="/src/main.ts"></script>
50
57
  </body>
51
58
  </html>
@@ -1,26 +1,48 @@
1
1
  import type { EntityElement } from '@playcanvas/web-components';
2
2
  import { whenReady } from '@playcanvas/web-components';
3
+ import type { Entity } from 'playcanvas';
4
+ import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
3
5
  import { Grid } from 'playcanvas/scripts/esm/grid.mjs';
4
6
 
5
7
  import { SceneEditor } from './scene-editor';
8
+ import type { Mode, Shape } from './scene-editor';
6
9
  import './starter.css';
7
10
 
8
- const [appElement, camera, grid, cube, sphere, editor] = await Promise.all([
11
+ // The camera is awaited as <pc-camera> rather than the <pc-entity> holding it - an entity becomes
12
+ // ready before its component children do, and the scripts need the camera component to exist
13
+ const [appElement, cameraComponent, grid, host] = await Promise.all([
9
14
  whenReady('pc-app'),
10
- whenReady<EntityElement>('pc-entity[name="camera"]'),
15
+ whenReady('pc-camera'),
11
16
  whenReady<EntityElement>('pc-entity[name="grid"]'),
12
- whenReady<EntityElement>('pc-entity[name="cube"]'),
13
- whenReady<EntityElement>('pc-entity[name="sphere"]'),
14
17
  whenReady<EntityElement>('pc-entity[name="editor"]')
15
18
  ]);
19
+ const app = appElement.app!;
20
+ const camera = cameraComponent.closestEntity!.entity!;
16
21
 
22
+ // Engine scripts are resolved by the bundler, so they are attached through the entity rather than
23
+ // with <pc-script-instance name="...">, which only resolves scripts fetched at runtime by <pc-asset>
24
+ camera.addComponent('script');
25
+ camera.script!.create(CameraControls, { properties: { sceneSize: 5 } });
17
26
  grid.entity!.addComponent('script');
18
27
  grid.entity!.script!.create(Grid);
19
- editor.entity!.addComponent('script');
20
- editor.entity!.script!.create(SceneEditor, {
21
- properties: {
22
- camera: camera.entity,
23
- targets: [cube.entity, sphere.entity],
24
- layer: appElement.app!.scene.layers.getLayerByName('World')
25
- }
28
+ host.entity!.addComponent('script');
29
+ const editor = host.entity!.script!.create(SceneEditor, { properties: { camera } }) as SceneEditor;
30
+
31
+ // The HUD drives the editor, and follows its selection
32
+ const modes = document.querySelectorAll<HTMLButtonElement>('[data-mode]');
33
+ modes.forEach((button) => {
34
+ button.addEventListener('click', () => {
35
+ editor.mode = button.dataset.mode as Mode;
36
+ modes.forEach((other) => (other.ariaPressed = String(other === button)));
37
+ });
38
+ });
39
+ document.querySelectorAll<HTMLButtonElement>('[data-add]').forEach((button) => {
40
+ button.addEventListener('click', () => editor.add(button.dataset.add as Shape));
41
+ });
42
+
43
+ const remove = document.getElementById('delete') as HTMLButtonElement;
44
+ remove.addEventListener('click', () => editor.remove());
45
+ document.getElementById('reset')!.addEventListener('click', () => editor.reset());
46
+ app.on('editor:select', (selected: Entity | null) => {
47
+ remove.disabled = !selected;
26
48
  });
@@ -1,7 +0,0 @@
1
- ## Interactive Sphere
2
-
3
- A lit sphere, an environment map, grid and orbit camera controls.
4
-
5
- ### Controls
6
-
7
- Drag to orbit the camera. Click the sphere to increment the counter.
@@ -1,19 +0,0 @@
1
- .counter {
2
- position: fixed;
3
- bottom: 22px;
4
- left: 50%;
5
- z-index: 2;
6
- margin: 0;
7
- padding: 9px 13px;
8
- border: 1px solid #ffffff24;
9
- border-radius: 999px;
10
- background: #07101bd9;
11
- color: #f8fafc;
12
- font:
13
- 650 12px Inter,
14
- ui-sans-serif,
15
- system-ui,
16
- sans-serif;
17
- translate: -50% 0;
18
- backdrop-filter: blur(10px);
19
- }
@@ -1,7 +0,0 @@
1
- ## Physics Playground
2
-
3
- A rigid-body scene with a static floor, orbit camera and dynamic primitive shapes.
4
-
5
- ### Controls
6
-
7
- Use **Spawn object** to add a body and **Reset** to recreate the starting set. Drag to orbit the camera.
@@ -1,182 +0,0 @@
1
- import type { Layer } from 'playcanvas';
2
- import {
3
- createGraphicsDevice,
4
- AppBase,
5
- AppOptions,
6
- RenderComponentSystem,
7
- CameraComponentSystem,
8
- ScriptComponentSystem,
9
- TextureHandler,
10
- ContainerHandler,
11
- FILLMODE_FILL_WINDOW,
12
- RESOLUTION_AUTO,
13
- Entity,
14
- Color,
15
- Vec3,
16
- Picker,
17
- EVENT_MOUSEMOVE,
18
- EVENT_MOUSEUP,
19
- TouchDevice,
20
- Mouse,
21
- StandardMaterial,
22
- Asset,
23
- AssetListLoader,
24
- TEXTURETYPE_RGBP
25
- } from 'playcanvas';
26
- import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
27
- import { Grid } from 'playcanvas/scripts/esm/grid.mjs';
28
-
29
- import { throttle } from './utils';
30
-
31
- const HOVER_COLOR = new Color(0.25, 0.8, 1);
32
- const DEFAULT_COLOR = new Color(0.9, 0.93, 0.98);
33
-
34
- // Assets to load
35
- const assets = {
36
- envAtlas: new Asset(
37
- 'env-atlas',
38
- 'texture',
39
- { url: './environment-map.png' },
40
- {
41
- type: TEXTURETYPE_RGBP,
42
- mipmaps: false
43
- }
44
- )
45
- };
46
-
47
- /**
48
- * Setup the PlayCanvas app
49
- * @param canvas - The canvas element
50
- * @param onClick - The function to call when the user clicks on the sphere
51
- */
52
- const setupApp = async (canvas: HTMLCanvasElement, onClick: () => void) => {
53
- if (!canvas) {
54
- throw new Error('Canvas not found');
55
- }
56
-
57
- // Create graphics device
58
- const device = await createGraphicsDevice(canvas);
59
-
60
- // Create app options
61
- const createOptions = new AppOptions();
62
- createOptions.graphicsDevice = device;
63
- createOptions.mouse = new Mouse(document.body);
64
- createOptions.touch = new TouchDevice(document.body);
65
- createOptions.componentSystems = [RenderComponentSystem, CameraComponentSystem, ScriptComponentSystem];
66
- createOptions.resourceHandlers = [TextureHandler, ContainerHandler];
67
-
68
- // Create app
69
- const app = new AppBase(canvas);
70
- app.init(createOptions);
71
-
72
- // Set the canvas to fill the window
73
- app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
74
- app.setCanvasResolution(RESOLUTION_AUTO);
75
-
76
- // Ensure canvas is resized when window changes size
77
- const resize = () => app.resizeCanvas();
78
- window.addEventListener('resize', resize);
79
-
80
- app.once('destroy', () => {
81
- window.removeEventListener('resize', resize);
82
- });
83
-
84
- // Load assets
85
- await new Promise<void>((resolve) => {
86
- new AssetListLoader(Object.values(assets), app.assets).load(() => resolve());
87
- });
88
-
89
- app.start();
90
-
91
- // Set up environment lighting (no skybox, just IBL)
92
- app.scene.envAtlas = assets.envAtlas.resource!;
93
- const skyboxLayer = app.scene.layers.getLayerByName('Skybox');
94
- if (skyboxLayer) {
95
- skyboxLayer.enabled = false;
96
- }
97
-
98
- // Create sphere entity
99
- const sphere = new Entity('sphere');
100
- sphere.setPosition(new Vec3(0, 0.7, 0));
101
- sphere.setLocalScale(1.25, 1.25, 1.25);
102
-
103
- // Create a new material
104
- const material = new StandardMaterial();
105
- material.diffuse.copy(DEFAULT_COLOR);
106
- material.update();
107
-
108
- sphere.addComponent('render', {
109
- type: 'sphere',
110
- material: material
111
- });
112
- app.root.addChild(sphere);
113
-
114
- // Create camera entity
115
- const camera = new Entity('camera');
116
- camera.addComponent('camera', {
117
- clearColor: new Color(0.025, 0.04, 0.065)
118
- });
119
- camera.setPosition(new Vec3(3.2, 1.5, 3.2));
120
- camera.lookAt(0, 0.7, 0);
121
- app.root.addChild(camera);
122
-
123
- // Create camera controls
124
- camera.addComponent('script');
125
- camera.script?.create(CameraControls, { properties: { sceneSize: 3 } });
126
-
127
- // Create grid entity
128
- const grid = new Entity('grid');
129
- grid.addComponent('script');
130
- grid.script?.create(Grid);
131
- grid.setLocalScale(12, 12, 12);
132
- app.root.addChild(grid);
133
-
134
- // Create a picker for mouse interaction
135
- const picker = new Picker(app, 1, 1);
136
- const worldLayer = app.scene.layers.getLayerByName('World');
137
-
138
- const intersectsSphere = (x: number, y: number, layer: Layer) => {
139
- if (!camera.camera) {
140
- return Promise.resolve(false);
141
- }
142
-
143
- const pickerScale = 0.5;
144
- picker.resize(canvas.clientWidth * pickerScale, canvas.clientHeight * pickerScale);
145
-
146
- if (!layer) {
147
- return Promise.resolve(false);
148
- }
149
-
150
- picker.prepare(camera.camera, app.scene, [layer]);
151
-
152
- return picker.getSelectionAsync(x * pickerScale, y * pickerScale, 1, 1).then((items) => {
153
- if (items.length === 0) return false;
154
- return items[0] === sphere.render?.meshInstances[0];
155
- });
156
- };
157
-
158
- // On mouse move, check if hovering over sphere and update cursor/color
159
- app.mouse?.on(
160
- EVENT_MOUSEMOVE,
161
- throttle((event) => {
162
- if (!worldLayer) return;
163
- intersectsSphere(event.x, event.y, worldLayer).then((intersects) => {
164
- material.diffuse.copy(intersects ? HOVER_COLOR : DEFAULT_COLOR);
165
- document.body.style.cursor = intersects ? 'pointer' : 'default';
166
- material.update();
167
- });
168
- }, 100)
169
- );
170
-
171
- // On mouse up, check if clicked on sphere and call onClick
172
- app.mouse?.on(EVENT_MOUSEUP, (event) => {
173
- if (!worldLayer || !onClick) return;
174
- intersectsSphere(event.x, event.y, worldLayer).then((intersects) => {
175
- if (intersects) {
176
- onClick();
177
- }
178
- });
179
- });
180
- };
181
-
182
- export { setupApp };
@@ -1,16 +0,0 @@
1
- import { setupApp } from './App';
2
- import './counter.css';
3
- import './starter.css';
4
-
5
- let count = 0;
6
-
7
- document.body.insertAdjacentHTML('beforeend', `<p id="counter" class="counter">Click count: ${count}</p>`);
8
-
9
- const counterElement = document.getElementById('counter')!;
10
-
11
- const increment = () => {
12
- count++;
13
- counterElement.textContent = `Click count: ${count}`;
14
- };
15
-
16
- setupApp(document.getElementById('application-canvas') as HTMLCanvasElement, increment);
@@ -1,16 +0,0 @@
1
- /**
2
- * Simple throttle helper
3
- * @param fn - The function to throttle
4
- * @param interval - The interval in milliseconds
5
- * @returns A throttled function
6
- */
7
- export const throttle = <T extends unknown[]>(fn: (...args: T) => void, interval = 100) => {
8
- let last = 0;
9
- return (...args: T) => {
10
- const now = Date.now();
11
- if (now - last >= interval) {
12
- last = now;
13
- fn(...args);
14
- }
15
- };
16
- };