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.
- package/README.md +69 -24
- package/dist/index.mjs +11 -11
- package/package.json +1 -1
- package/templates/_shared/scene-editor/AGENTS.md +8 -0
- package/templates/_shared/scene-editor/README.md +15 -2
- package/templates/_shared/scene-editor/src/scene-editor.ts +186 -36
- package/templates/engine/ar-placement/index.html +20 -0
- package/templates/engine/ar-placement/src/main.ts +22 -12
- package/templates/engine/base/AGENTS.md +1 -0
- package/templates/engine/base/README.md +1 -1
- package/templates/engine/first-person-controller/index.html +19 -0
- package/templates/engine/first-person-controller/src/main.ts +16 -11
- package/templates/engine/product-configurator/index.html +30 -0
- package/templates/engine/product-configurator/src/main.ts +33 -32
- package/templates/engine/scene-editor/index.html +33 -0
- package/templates/engine/scene-editor/src/main.ts +56 -47
- package/templates/engine/sprite-game/index.html +19 -0
- package/templates/engine/sprite-game/src/main.ts +19 -11
- package/templates/engine/third-person-controller/index.html +19 -0
- package/templates/engine/third-person-controller/src/main.ts +16 -11
- package/templates/engine/vr-starter/index.html +20 -0
- package/templates/engine/vr-starter/src/main.ts +22 -12
- package/templates/index.ts +1 -14
- package/templates/react/base/AGENTS.md +1 -0
- package/templates/react/scene-editor/src/App.tsx +73 -40
- package/templates/web-components/base/AGENTS.md +1 -0
- package/templates/web-components/scene-editor/index.html +32 -25
- package/templates/web-components/scene-editor/src/main.ts +33 -11
- package/templates/_shared/interactive-sphere/README.md +0 -7
- package/templates/_shared/interactive-sphere/public/environment-map.png +0 -0
- package/templates/_shared/interactive-sphere/src/counter.css +0 -19
- package/templates/_shared/physics-playground/README.md +0 -7
- package/templates/engine/interactive-sphere/src/App.ts +0 -182
- package/templates/engine/interactive-sphere/src/main.ts +0 -16
- package/templates/engine/interactive-sphere/src/utils.ts +0 -16
- package/templates/engine/physics-playground/src/main.ts +0 -131
- package/templates/react/interactive-sphere/src/App.tsx +0 -21
- package/templates/react/interactive-sphere/src/Scene.tsx +0 -70
- package/templates/react/physics-playground/src/App.tsx +0 -85
- package/templates/web-components/interactive-sphere/index.html +0 -36
- package/templates/web-components/interactive-sphere/src/main.ts +0 -76
- package/templates/web-components/physics-playground/index.html +0 -46
- 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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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(
|
|
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());
|
package/templates/index.ts
CHANGED
|
@@ -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: '
|
|
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,
|
|
3
|
-
import { useApp,
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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}
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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>
|
|
86
|
+
<p>Click an object to select it, then drag its gizmo. Drag anywhere else to orbit.</p>
|
|
65
87
|
<div className="controls">
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
73
|
-
|
|
106
|
+
<button disabled={!selected} onClick={() => editor.current?.remove()}>
|
|
107
|
+
Delete
|
|
74
108
|
</button>
|
|
75
|
-
<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="
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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>
|
|
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-
|
|
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-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
20
|
-
editor.entity!.script!.create(SceneEditor, {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
});
|
|
Binary file
|
|
@@ -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,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
|
-
};
|