create-playcanvas 0.10.1 → 0.11.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.
@@ -0,0 +1,44 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ // Approximates the Web Lens sandbox, which allows resources from the Lens itself and nothing else
4
+ const LENS_SANDBOX = [
5
+ "default-src 'self' data: blob:",
6
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' blob:",
7
+ "style-src 'self' 'unsafe-inline'",
8
+ "frame-src 'none'",
9
+ "object-src 'none'",
10
+ "base-uri 'none'",
11
+ "form-action 'none'"
12
+ ].join('; ');
13
+
14
+ // https://vite.dev/config/
15
+ export default defineConfig({
16
+ // Snapchat serves a Lens from its own origin, so every URL in the build must be relative
17
+ base: './',
18
+ build: {
19
+ // npm run package zips this folder and writes the zip beside it, inside the ignored dist/
20
+ outDir: 'dist/lens',
21
+ // source maps only add to the 8 MB upload
22
+ sourcemap: false
23
+ },
24
+ preview: {
25
+ // npm run start serves the build under the Lens rules, so a request for anything outside
26
+ // the zip fails in the browser console as it would in Snapchat
27
+ headers: {
28
+ 'Content-Security-Policy': LENS_SANDBOX,
29
+ 'Cross-Origin-Opener-Policy': 'same-origin',
30
+ 'Cross-Origin-Embedder-Policy': 'require-corp'
31
+ }
32
+ },
33
+ plugins: [
34
+ {
35
+ // Dev only: install the PlayCanvas Inspector before the app starts (press ` to toggle)
36
+ name: 'playcanvas-inspector',
37
+ apply: 'serve',
38
+ config: () => ({ optimizeDeps: { include: ['@playcanvas/inspector'] } }),
39
+ transformIndexHtml: () => [
40
+ { tag: 'script', attrs: { type: 'module', src: '/src/inspector.ts' }, injectTo: 'head-prepend' }
41
+ ]
42
+ }
43
+ ]
44
+ });
@@ -90,6 +90,12 @@ export const STARTERS = [
90
90
  category: 'Games',
91
91
  description: 'an orthographic camera, sprite animation and input'
92
92
  },
93
+ {
94
+ name: 'web-lens',
95
+ display: 'Snapchat Web Lens',
96
+ category: 'Games',
97
+ description: 'a portrait game with Snapchat leaderboards, packaged for upload'
98
+ },
93
99
  {
94
100
  name: 'scene-editor',
95
101
  display: 'Scene Editor',
@@ -16,20 +16,20 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@playcanvas/react": "^0.11.7",
19
- "playcanvas": "^2.23.1",
20
- "react": "^19.3.0",
21
- "react-dom": "^19.3.0"
19
+ "playcanvas": "^2.23.3",
20
+ "react": "^19.3.1",
21
+ "react-dom": "^19.3.1"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@playcanvas/eslint-config": "3.0.0-beta.8",
25
- "@playcanvas/inspector": "^0.2.0",
25
+ "@playcanvas/inspector": "^0.3.0",
26
26
  "@types/react": "^19.3.0",
27
27
  "@types/react-dom": "^19.3.0",
28
28
  "@vitejs/plugin-react": "^6.1.2",
29
29
  "eslint": "^9.39.5",
30
- "prettier": "^3.9.9",
30
+ "prettier": "^3.9.10",
31
31
  "typescript": "~6.0.3",
32
- "vite": "^8.3.4"
32
+ "vite": "^8.3.5"
33
33
  },
34
34
  "engines": {
35
35
  "node": ">=22.23.2"
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
+ <title>Dodge</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,85 @@
1
+ import { Application, Entity } from '@playcanvas/react';
2
+ import { Camera, Light, Script } from '@playcanvas/react/components';
3
+ import { useApp, useAppEvent } from '@playcanvas/react/hooks';
4
+ import { useCallback, useEffect, useState } from 'react';
5
+
6
+ import { CARDS, DodgeGame } from './dodge-game';
7
+ import type { GameEvents, GameState } from './dodge-game';
8
+ import { hasLeaderboard, loadAvatar, loadBest, recordScore, showLeaderboard } from './lens';
9
+ import './lens.css';
10
+
11
+ function Scene({ onState }: { onState: GameEvents['game:state'] }) {
12
+ const app = useApp();
13
+ useAppEvent<GameEvents>('game:state', onState);
14
+
15
+ useEffect(() => {
16
+ // render at up to twice the CSS resolution: sharp on a phone, without the cost of 3x
17
+ // eslint-disable-next-line react-hooks/immutability -- the device belongs to the engine, not React
18
+ app.graphicsDevice.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
19
+ }, [app]);
20
+
21
+ return (
22
+ <>
23
+ {/* fix the field's width, so a taller phone shows more of the falling blocks */}
24
+ <Entity name="camera" position={[0, 0, 7]}>
25
+ <Camera clearColor="#0b1020" fov={45} horizontalFov />
26
+ </Entity>
27
+ <Entity name="light" rotation={[40, 30, 0]}>
28
+ <Light type="directional" intensity={1.5} />
29
+ </Entity>
30
+ <Entity name="game">
31
+ <Script script={DodgeGame} />
32
+ </Entity>
33
+ </>
34
+ );
35
+ }
36
+
37
+ function App() {
38
+ const [state, setState] = useState<GameState>('ready');
39
+ const [score, setScore] = useState(0);
40
+ const [best, setBest] = useState(0);
41
+ const [avatar, setAvatar] = useState<string>();
42
+
43
+ useEffect(() => {
44
+ loadBest().then(setBest);
45
+ loadAvatar().then(setAvatar);
46
+ }, []);
47
+
48
+ // the HUD follows the game's state, and the Lens records each finished run
49
+ const onState = useCallback((next: GameState, value: number) => {
50
+ setState(next);
51
+ setScore(value);
52
+ if (next === 'over') recordScore(value).then(setBest);
53
+ }, []);
54
+
55
+ const card = CARDS[state];
56
+
57
+ return (
58
+ <main className="lens">
59
+ {/* lens.css sizes the canvas, filling a phone screen and centering a 9:16 column anywhere wider */}
60
+ <Application>
61
+ <Scene onState={onState} />
62
+ </Application>
63
+ <div className="lens-hud">
64
+ <header className="lens-bar">
65
+ {avatar && <img className="lens-avatar" src={avatar} alt="Your Bitmoji" />}
66
+ <span className="lens-score">{score}</span>
67
+ <span className="lens-best">Best {best}</span>
68
+ </header>
69
+ {card && (
70
+ <section className="lens-card">
71
+ <h1 className="lens-title">{card.title}</h1>
72
+ {state === 'over' && hasLeaderboard() && (
73
+ <button className="lens-leaderboard" onClick={showLeaderboard}>
74
+ Leaderboard
75
+ </button>
76
+ )}
77
+ <p className="lens-hint">{card.hint}</p>
78
+ </section>
79
+ )}
80
+ </div>
81
+ </main>
82
+ );
83
+ }
84
+
85
+ export default App;
@@ -0,0 +1,46 @@
1
+ import react from '@vitejs/plugin-react';
2
+ import { defineConfig } from 'vite';
3
+
4
+ // Approximates the Web Lens sandbox, which allows resources from the Lens itself and nothing else
5
+ const LENS_SANDBOX = [
6
+ "default-src 'self' data: blob:",
7
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' blob:",
8
+ "style-src 'self' 'unsafe-inline'",
9
+ "frame-src 'none'",
10
+ "object-src 'none'",
11
+ "base-uri 'none'",
12
+ "form-action 'none'"
13
+ ].join('; ');
14
+
15
+ // https://vite.dev/config/
16
+ export default defineConfig({
17
+ // Snapchat serves a Lens from its own origin, so every URL in the build must be relative
18
+ base: './',
19
+ build: {
20
+ // npm run package zips this folder and writes the zip beside it, inside the ignored dist/
21
+ outDir: 'dist/lens',
22
+ // source maps only add to the 8 MB upload
23
+ sourcemap: false
24
+ },
25
+ preview: {
26
+ // npm run start serves the build under the Lens rules, so a request for anything outside
27
+ // the zip fails in the browser console as it would in Snapchat
28
+ headers: {
29
+ 'Content-Security-Policy': LENS_SANDBOX,
30
+ 'Cross-Origin-Opener-Policy': 'same-origin',
31
+ 'Cross-Origin-Embedder-Policy': 'require-corp'
32
+ }
33
+ },
34
+ plugins: [
35
+ react(),
36
+ {
37
+ // Dev only: install the PlayCanvas Inspector before the app starts (press ` to toggle)
38
+ name: 'playcanvas-inspector',
39
+ apply: 'serve',
40
+ config: () => ({ optimizeDeps: { include: ['@playcanvas/inspector'] } }),
41
+ transformIndexHtml: () => [
42
+ { tag: 'script', attrs: { type: 'module', src: '/src/inspector.ts' }, injectTo: 'head-prepend' }
43
+ ]
44
+ }
45
+ ]
46
+ });
@@ -16,15 +16,15 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@playcanvas/web-components": "^0.28.0",
19
- "playcanvas": "^2.23.1"
19
+ "playcanvas": "^2.23.3"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@playcanvas/eslint-config": "3.0.0-beta.8",
23
- "@playcanvas/inspector": "^0.2.0",
23
+ "@playcanvas/inspector": "^0.3.0",
24
24
  "eslint": "^9.39.5",
25
- "prettier": "^3.9.9",
25
+ "prettier": "^3.9.10",
26
26
  "typescript": "~6.0.3",
27
- "vite": "^8.3.4"
27
+ "vite": "^8.3.5"
28
28
  },
29
29
  "engines": {
30
30
  "node": ">=22.23.2"
@@ -0,0 +1,36 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
+ <title>Dodge</title>
7
+ </head>
8
+ <body>
9
+ <main class="lens">
10
+ <pc-app max-pixel-ratio="2">
11
+ <pc-scene>
12
+ <pc-entity name="camera" position="0 0 7">
13
+ <pc-camera clear-color="#0b1020" fov="45" horizontal-fov></pc-camera>
14
+ </pc-entity>
15
+ <pc-entity name="light" rotation="40 30 0">
16
+ <pc-light type="directional" intensity="1.5"></pc-light>
17
+ </pc-entity>
18
+ <pc-entity name="game"></pc-entity>
19
+ </pc-scene>
20
+ </pc-app>
21
+ <div class="lens-hud">
22
+ <header class="lens-bar">
23
+ <img class="lens-avatar" alt="Your Bitmoji" hidden />
24
+ <span class="lens-score">0</span>
25
+ <span class="lens-best">Best 0</span>
26
+ </header>
27
+ <section class="lens-card">
28
+ <h1 class="lens-title">Dodge</h1>
29
+ <button class="lens-leaderboard" hidden>Leaderboard</button>
30
+ <p class="lens-hint">Drag to steer. Tap to play</p>
31
+ </section>
32
+ </div>
33
+ </main>
34
+ <script type="module" src="/src/main.ts"></script>
35
+ </body>
36
+ </html>
@@ -0,0 +1,47 @@
1
+ import type { EntityElement } from '@playcanvas/web-components';
2
+ import { whenReady } from '@playcanvas/web-components';
3
+
4
+ import { CARDS, DodgeGame } from './dodge-game';
5
+ import type { GameState } from './dodge-game';
6
+ import { hasLeaderboard, loadAvatar, loadBest, recordScore, showLeaderboard } from './lens';
7
+ import './lens.css';
8
+
9
+ // the HUD follows the game's state, and the Lens records each finished run
10
+ const hud = {
11
+ avatar: document.querySelector<HTMLImageElement>('.lens-avatar')!,
12
+ score: document.querySelector('.lens-score')!,
13
+ best: document.querySelector('.lens-best')!,
14
+ card: document.querySelector<HTMLElement>('.lens-card')!,
15
+ title: document.querySelector('.lens-title')!,
16
+ hint: document.querySelector('.lens-hint')!,
17
+ leaderboard: document.querySelector<HTMLButtonElement>('.lens-leaderboard')!
18
+ };
19
+ const showBest = (best: number) => {
20
+ hud.best.textContent = `Best ${best}`;
21
+ };
22
+
23
+ const { app } = await whenReady('pc-app');
24
+ app!.on('game:state', (state: GameState, score: number) => {
25
+ const card = CARDS[state];
26
+ hud.score.textContent = String(score);
27
+ hud.card.hidden = !card;
28
+ if (card) {
29
+ hud.title.textContent = card.title;
30
+ hud.hint.textContent = card.hint;
31
+ }
32
+ hud.leaderboard.hidden = state !== 'over' || !hasLeaderboard();
33
+ if (state === 'over') recordScore(score).then(showBest);
34
+ });
35
+ hud.leaderboard.addEventListener('click', showLeaderboard);
36
+
37
+ loadBest().then(showBest);
38
+ loadAvatar().then((url) => {
39
+ if (!url) return;
40
+ hud.avatar.src = url;
41
+ hud.avatar.hidden = false;
42
+ });
43
+
44
+ // the game is an engine script imported through the bundler, so it attaches through the entity
45
+ const game = await whenReady<EntityElement>('pc-entity[name="game"]');
46
+ game.entity!.addComponent('script');
47
+ game.entity!.script!.create(DodgeGame);
@@ -0,0 +1,44 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ // Approximates the Web Lens sandbox, which allows resources from the Lens itself and nothing else
4
+ const LENS_SANDBOX = [
5
+ "default-src 'self' data: blob:",
6
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' blob:",
7
+ "style-src 'self' 'unsafe-inline'",
8
+ "frame-src 'none'",
9
+ "object-src 'none'",
10
+ "base-uri 'none'",
11
+ "form-action 'none'"
12
+ ].join('; ');
13
+
14
+ // https://vite.dev/config/
15
+ export default defineConfig({
16
+ // Snapchat serves a Lens from its own origin, so every URL in the build must be relative
17
+ base: './',
18
+ build: {
19
+ // npm run package zips this folder and writes the zip beside it, inside the ignored dist/
20
+ outDir: 'dist/lens',
21
+ // source maps only add to the 8 MB upload
22
+ sourcemap: false
23
+ },
24
+ preview: {
25
+ // npm run start serves the build under the Lens rules, so a request for anything outside
26
+ // the zip fails in the browser console as it would in Snapchat
27
+ headers: {
28
+ 'Content-Security-Policy': LENS_SANDBOX,
29
+ 'Cross-Origin-Opener-Policy': 'same-origin',
30
+ 'Cross-Origin-Embedder-Policy': 'require-corp'
31
+ }
32
+ },
33
+ plugins: [
34
+ {
35
+ // Dev only: install the PlayCanvas Inspector before the app starts (press ` to toggle)
36
+ name: 'playcanvas-inspector',
37
+ apply: 'serve',
38
+ config: () => ({ optimizeDeps: { include: ['@playcanvas/inspector'] } }),
39
+ transformIndexHtml: () => [
40
+ { tag: 'script', attrs: { type: 'module', src: '/src/inspector.ts' }, injectTo: 'head-prepend' }
41
+ ]
42
+ }
43
+ ]
44
+ });