@vibes.diy/prompts 14.3.58 → 14.3.59

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.
@@ -1,196 +0,0 @@
1
- # Voxel worlds — the first draft: one grass block, turning in the sky
2
-
3
- A voxel app's first draft is a full-screen 3D viewport with the HUD floating
4
- over it, and in that viewport ONE block: a pixel-art grass block, green on top
5
- and dirt on the sides, turning slowly under a blue sky. The name card, the
6
- hotbar, the palette, the seeded world document and the look are all finished;
7
- the block is real, lit and drawn through the same instanced path the whole
8
- world will use.
9
- Terrain, placing and breaking, walking, saving edits and sharing the world are
10
- the next pass. What the draft owes that pass is the right skeleton: the engine
11
- is built once inside an effect, the block is ONE `InstancedMesh` with a
12
- count of 1, the viewport is `fixed inset-0` with the controls on top of it,
13
- and the palette is named once for the HUD and the blocks, so the next pass
14
- raises the count and adds to this file rather than rewriting it.
15
-
16
- Unlike the page-shaped draft above, a world does not flow down the page: the
17
- viewport is `fixed inset-0` and the controls sit on top of it.
18
-
19
- ### `App.jsx`
20
- ```jsx
21
- import React, { useEffect, useRef, useState } from "react";
22
- import { useFireproof } from "use-fireproof";
23
- import * as THREE from "three";
24
- import { Button, Badge } from "@vibes.diy/look";
25
-
26
- // The block-in-hand palette (shared by block colors and HUD swatches).
27
- const VOXEL_PALETTE = {
28
- grass: "#6fcf5b", dirt: "#8a5a3b", stone: "#9b9b9b", wood: "#b5793a", glass: "#9fe3ff", candy: "#ff6fb1",
29
- neon: "#39ff9e", water: "#2f8fe0", lava: "#ff5a2f", erase: "#ff4d6d", ink: "#1a1325", light: "#ffffff",
30
- };
31
- const HOTBAR = ["grass", "dirt", "stone", "wood", "glass", "candy"];
32
- const SKY = 0x8ecbff;
33
-
34
- // A 16×16 pixel-art face, painted once. `fringe` rows of green sit on top of dirt for the sides.
35
- function blockFace(kind) {
36
- const canvas = document.createElement("canvas");
37
- canvas.width = 16;
38
- canvas.height = 16;
39
- const ctx = canvas.getContext("2d");
40
- const px = (x, y, color) => { ctx.fillStyle = color; ctx.fillRect(x, y, 1, 1); };
41
- let n = kind.length * 7;
42
- const rand = () => { n = (n * 9301 + 49297) % 233280; return n / 233280; };
43
- for (let y = 0; y < 16; y++)
44
- for (let x = 0; x < 16; x++) {
45
- const fringe = kind === "side" && y < 3 + (x % 3 === 0 ? 1 : 0);
46
- if (kind === "top" || fringe) px(x, y, rand() < 0.18 ? "#4fa83f" : VOXEL_PALETTE.grass);
47
- else px(x, y, rand() < 0.2 ? "#6e4630" : VOXEL_PALETTE.dirt);
48
- }
49
- const texture = new THREE.CanvasTexture(canvas);
50
- texture.magFilter = THREE.NearestFilter;
51
- texture.minFilter = THREE.NearestFilter;
52
- texture.colorSpace = THREE.SRGBColorSpace;
53
- return texture;
54
- }
55
-
56
- export default function App() {
57
- const { useLiveQuery } = useFireproof("skyIsland");
58
- const { docs: worlds } = useLiveQuery("type", { key: "world" });
59
- const world = worlds[0];
60
- const mountRef = useRef(null);
61
- const [held, setHeld] = useState(HOTBAR[0]);
62
-
63
- // The engine, built once: renderer, scene, lights, the instanced block, the loop. React only draws the HUD over it.
64
- useEffect(() => {
65
- const mount = mountRef.current;
66
- const renderer = new THREE.WebGLRenderer({ antialias: true });
67
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
68
- renderer.setSize(mount.clientWidth, mount.clientHeight);
69
- mount.appendChild(renderer.domElement);
70
-
71
- const scene = new THREE.Scene();
72
- scene.background = new THREE.Color(SKY);
73
- scene.fog = new THREE.Fog(SKY, 8, 30);
74
- const camera = new THREE.PerspectiveCamera(60, mount.clientWidth / mount.clientHeight, 0.1, 100);
75
- camera.position.set(0, 1.4, 3.2);
76
- camera.lookAt(0, 0, 0);
77
- scene.add(new THREE.HemisphereLight(0xffffff, 0x8a5a3b, 0.9));
78
- const sun = new THREE.DirectionalLight(0xffffff, 1.2);
79
- sun.position.set(4, 8, 5);
80
- scene.add(sun);
81
-
82
- // The hello: one grass block, drawn the way the whole world will be — one shared cube, six shared
83
- // face materials (BoxGeometry face order is +x, -x, +y, -y, +z, -z), one InstancedMesh. The next pass
84
- // raises the count and fills more matrices; it never adds a Mesh per block.
85
- const textures = { top: blockFace("top"), side: blockFace("side"), bottom: blockFace("bottom") };
86
- const faces = ["side", "side", "top", "bottom", "side", "side"];
87
- const materials = faces.map((f) => new THREE.MeshLambertMaterial({ map: textures[f] }));
88
- const cube = new THREE.BoxGeometry(1, 1, 1);
89
- const blocks = new THREE.InstancedMesh(cube, materials, 1);
90
- scene.add(blocks);
91
- const matrix = new THREE.Matrix4();
92
- const spin = new THREE.Quaternion();
93
- const up = new THREE.Vector3(0, 1, 0);
94
- const at = new THREE.Vector3();
95
- const one = new THREE.Vector3(1, 1, 1);
96
-
97
- const onResize = () => {
98
- renderer.setSize(mount.clientWidth, mount.clientHeight);
99
- camera.aspect = mount.clientWidth / mount.clientHeight;
100
- camera.updateProjectionMatrix();
101
- };
102
- window.addEventListener("resize", onResize);
103
-
104
- const clock = new THREE.Clock();
105
- let raf;
106
- const loop = () => {
107
- const t = clock.getElapsedTime();
108
- spin.setFromAxisAngle(up, t * 0.5);
109
- at.set(0, Math.sin(t * 1.2) * 0.06, 0);
110
- blocks.setMatrixAt(0, matrix.compose(at, spin, one));
111
- blocks.instanceMatrix.needsUpdate = true;
112
- renderer.render(scene, camera);
113
- raf = requestAnimationFrame(loop);
114
- };
115
- loop();
116
-
117
- return () => {
118
- cancelAnimationFrame(raf);
119
- window.removeEventListener("resize", onResize);
120
- Object.values(textures).forEach((t) => t.dispose());
121
- materials.forEach((m) => m.dispose());
122
- blocks.dispose();
123
- cube.dispose();
124
- renderer.dispose();
125
- mount.removeChild(renderer.domElement);
126
- };
127
- }, []);
128
-
129
- return (
130
- <div className="fixed inset-0 select-none touch-none bg-background text-foreground font-sans">
131
- <div ref={mountRef} className="absolute inset-0" />
132
-
133
- <div className="absolute left-4 top-4 rounded-lg bg-card/90 px-3 py-2 text-card-foreground shadow-md">
134
- <p className="font-bold">{world?.name ?? "Sky Island"}</p>
135
- <p className="text-muted-foreground">Your first block is ready. Pick what goes in your hand.</p>
136
- </div>
137
-
138
- <Badge className="absolute right-4 top-4">Holding {held}</Badge>
139
-
140
- <div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 gap-2 rounded-lg bg-card/90 p-2 shadow-md">
141
- {HOTBAR.map((type) => (
142
- <Button key={type} size="sm" variant={type === held ? "default" : "outline"} onClick={() => setHeld(type)}
143
- aria-label={type}>
144
- <span className="inline-block h-4 w-4 rounded-sm" style={{ backgroundColor: VOXEL_PALETTE[type] }} />
145
- {type}
146
- </Button>
147
- ))}
148
- </div>
149
- {/* TODO(next pass): the flat block map and instanced terrain, place and break the held block, walk with pointer lock, save edits as documents, share the world */}
150
- </div>
151
- );
152
- }
153
- ```
154
-
155
- ### `seed.json`
156
- ```json
157
- {
158
- "skyIsland": [
159
- { "key": "island", "type": "world", "name": "Sky Island", "sky": "clear" }
160
- ]
161
- }
162
- ```
163
-
164
- Draft note: the next pass adds the terrain around this block, placing and breaking with the held block, walking the world, saving edits, and sharing the world with friends.
165
-
166
- ## What stays the same in a voxel draft
167
-
168
- - **The viewport fills the screen**: the outer element is `fixed inset-0
169
- select-none touch-none`, the renderer mounts into an `absolute inset-0` div,
170
- and every control is an `absolute` layer over it. Never the page-flow `main`
171
- with padding from the shape above.
172
- - **The engine is built once** in `useEffect(() => {...}, [])` with the
173
- renderer, scene, lights, listeners and loop inside it, and a cleanup that
174
- stops the loop, removes the listener and disposes every texture, material, the
175
- instanced mesh, the geometry and the renderer. Per-frame values never live in React state.
176
- - **`setPixelRatio(Math.min(window.devicePixelRatio, 2))`**, a resize handler
177
- that updates the renderer size and the camera aspect, a hemisphere plus a
178
- directional light, and a `Fog` that matches the sky color.
179
- - **The hello is one grass block, turning, through the fast path**: one shared
180
- `BoxGeometry` and an array of six shared materials, each face a 16×16
181
- `THREE.CanvasTexture` painted in code with `NearestFilter` so the pixels
182
- stay crisp, drawn as `new THREE.InstancedMesh(cube, materials, 1)` and
183
- turned each frame with `setMatrixAt(0, …)` plus
184
- `instanceMatrix.needsUpdate = true`. No texture URLs, no loaders.
185
- - **The palette table** is the Blocktopia voxel palette, in full, named once
186
- for the block colors and the HUD swatches.
187
- - **The next pass keeps this skeleton**: the world becomes a flat `Map` keyed
188
- `"x|y|z"`, drawn through the same ONE `InstancedMesh` of the exposed blocks
189
- (the count raised, a matrix per block) with its shared geometry and
190
- materials. Never a
191
- `THREE.Mesh` per block, and never a geometry or material created inside the
192
- block loop. Pointer lock comes from a click-to-play overlay whose visibility
193
- follows the real lock state, and movement keys are the game's own set.
194
- - **What waits for the next pass** is named in the `TODO(next pass)` comment
195
- and the draft note: terrain, placing and breaking, walking, saving edits,
196
- sharing. The draft draws none of it.
@@ -1,180 +0,0 @@
1
- # Web Audio — the first draft: one tone, played by a tap
2
-
3
- A sound app's first draft is the whole page around the instrument: the name,
4
- a row of named notes, a volume slider, a big Play button, and the last few
5
- notes played, listed live from the database. The capability's hello is real:
6
- every tap plays one short, shaped tone through the speakers, on iOS too. The
7
- sequencer or metronome on the audio clock, the effects chain and recording are
8
- the next pass. What the draft owes that pass is the right skeleton: one
9
- `AudioContext` in a ref, created inside the first gesture and resumed on every
10
- one after it, and a plain `playTone` helper that takes the context and a note,
11
- so the next pass adds a scheduler beside it rather than rewriting it.
12
-
13
- Web Audio is a browser built-in: there is no import, the constructor is
14
- `window.AudioContext` with the `window.webkitAudioContext` fallback.
15
-
16
- ### `App.jsx`
17
- ```jsx
18
- import React, { useEffect, useRef, useState } from "react";
19
- import { useFireproof } from "use-fireproof";
20
- import { Button, Badge, Card, CardHeader, CardTitle, CardContent } from "@vibes.diy/look";
21
-
22
- // Named notes of a C major pentatonic, so any order of taps sounds sweet.
23
- const NOTES = [
24
- { name: "C4", hz: 261.63 },
25
- { name: "D4", hz: 293.66 },
26
- { name: "E4", hz: 329.63 },
27
- { name: "G4", hz: 392.0 },
28
- { name: "A4", hz: 440.0 },
29
- { name: "C5", hz: 523.25 },
30
- ];
31
- const hzOf = (name) => (NOTES.find((n) => n.name === name) ?? NOTES[4]).hz;
32
- const timeOf = (ms) => new Date(ms).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit", second: "2-digit" });
33
-
34
- // The iOS-safe gate: create or resume the context and return it. Call it FIRST,
35
- // synchronously, inside the click handler, with no await before the sound starts.
36
- function unlockAudio(ctxRef) {
37
- ctxRef.current ||= new (window.AudioContext || window.webkitAudioContext)();
38
- const ctx = ctxRef.current;
39
- if (ctx.state !== "running") ctx.resume(); // iOS suspends after lock or backgrounding
40
- return ctx;
41
- }
42
-
43
- // One short tone: oscillator -> gain -> speakers, with an attack and a release, stopped at ~0.4 s.
44
- function playTone(ctx, hz, volume, wave) {
45
- const t = ctx.currentTime;
46
- const peak = Math.max(volume, 0.0001); // exponential ramps never touch zero
47
- const osc = ctx.createOscillator();
48
- const gain = ctx.createGain();
49
- osc.type = wave;
50
- osc.frequency.setValueAtTime(hz, t);
51
- gain.gain.setValueAtTime(0.0001, t);
52
- gain.gain.linearRampToValueAtTime(peak, t + 0.02); // attack
53
- gain.gain.exponentialRampToValueAtTime(0.0001, t + 0.4); // release
54
- osc.connect(gain).connect(ctx.destination);
55
- osc.start(t);
56
- osc.stop(t + 0.42);
57
- osc.onended = () => {
58
- osc.disconnect();
59
- gain.disconnect();
60
- };
61
- }
62
-
63
- export default function App() {
64
- const { database, useLiveQuery } = useFireproof("glassHarp");
65
- const { docs: instruments } = useLiveQuery("type", { key: "instrument" });
66
- // Only play documents carry createdAt; newest first, a handful of them.
67
- const { docs: plays } = useLiveQuery("createdAt", { descending: true, limit: 6 });
68
- const instrument = instruments[0];
69
- const ctxRef = useRef(null);
70
- const [note, setNote] = useState("A4");
71
- const [volume, setVolume] = useState(0.5);
72
-
73
- // The context is created by the first tap and closed when the app goes away.
74
- useEffect(() => {
75
- return () => {
76
- ctxRef.current?.close();
77
- ctxRef.current = null;
78
- };
79
- }, []);
80
-
81
- const play = (name) => {
82
- const ctx = unlockAudio(ctxRef); // same trusted handler, before anything async
83
- playTone(ctx, hzOf(name), volume, instrument?.wave ?? "triangle");
84
- setNote(name);
85
- // The sound is already playing; saving can take its time.
86
- database.put({ type: "play", note: name, createdAt: Date.now() });
87
- if (instrument) database.put({ ...instrument, lastNote: name });
88
- };
89
-
90
- return (
91
- <main className="min-h-screen bg-background text-foreground font-sans p-6">
92
- <h1 className="text-3xl font-bold">{instrument?.name ?? "Glass Harp"}</h1>
93
- <p className="mt-1 text-muted-foreground">Pick a note, press Play, and let it ring.</p>
94
-
95
- <Card className="mt-6">
96
- <CardHeader>
97
- <CardTitle>Play a note</CardTitle>
98
- </CardHeader>
99
- <CardContent>
100
- <div className="grid gap-5">
101
- <div className="flex flex-wrap gap-2">
102
- {NOTES.map((n) => (
103
- <Button key={n.name} variant={n.name === note ? "default" : "outline"} onClick={() => play(n.name)}>
104
- {n.name}
105
- </Button>
106
- ))}
107
- </div>
108
- <label className="grid gap-2">
109
- <span className="text-muted-foreground">Volume {Math.round(volume * 100)}%</span>
110
- <input type="range" min={0} max={1} step={0.05} value={volume} onChange={(e) => setVolume(Number(e.target.value))} className="w-full" />
111
- </label>
112
- <Button size="lg" onClick={() => play(note)}>
113
- Play {note}
114
- </Button>
115
- </div>
116
- </CardContent>
117
- </Card>
118
-
119
- <Card className="mt-6">
120
- <CardHeader>
121
- <CardTitle>Just played</CardTitle>
122
- </CardHeader>
123
- <CardContent>
124
- <div className="flex flex-wrap gap-2">
125
- {plays.map((p) => (
126
- <Badge key={p._id} variant="secondary">
127
- {p.note} · {timeOf(p.createdAt)}
128
- </Badge>
129
- ))}
130
- </div>
131
- <p className="mt-3 text-muted-foreground">Last note: {instrument?.lastNote ?? note}</p>
132
- </CardContent>
133
- </Card>
134
-
135
- {/* TODO(next pass): a step sequencer and metronome scheduled ahead on ctx.currentTime, a delay with effects in its feedback loop, and recording a take */}
136
- </main>
137
- );
138
- }
139
- ```
140
-
141
- ### `seed.json`
142
- ```json
143
- {
144
- "glassHarp": [
145
- { "key": "harp", "type": "instrument", "name": "Glass Harp", "wave": "triangle", "lastNote": "G4" },
146
- { "key": "play-1", "type": "play", "note": "C4", "createdAt": 1758400000000 },
147
- { "key": "play-2", "type": "play", "note": "E4", "createdAt": 1758400001000 },
148
- { "key": "play-3", "type": "play", "note": "G4", "createdAt": 1758400002000 }
149
- ]
150
- }
151
- ```
152
-
153
- Draft note: the next pass adds a step sequencer and metronome on the audio clock, a delay and filter chain, and recording a take to keep.
154
-
155
- ## What stays the same in a Web Audio draft
156
-
157
- - **One `AudioContext`, in a ref, made by a gesture**: `unlockAudio(ctxRef)`
158
- creates it on the first tap, re-checks `ctx.state` and resumes on every tap
159
- after, and the effect's cleanup closes it. Never a context at module scope,
160
- in `useState`, or in a mount effect.
161
- - **The iOS rule holds in every handler**: the unlock and the first
162
- `osc.start()` run synchronously inside the same click, before any `await`,
163
- timer, promise or state-driven effect. Saving to the database comes after
164
- the sound has started.
165
- - **The hello is one shaped tone**: an `OscillatorNode` into a `GainNode`,
166
- `setValueAtTime` to a floor of `0.0001`, `linearRampToValueAtTime` for the
167
- attack, `exponentialRampToValueAtTime` for the release, `osc.stop` at about
168
- 0.4 s, and both nodes disconnected in `onended`. Nothing plays on load.
169
- - **`playTone(ctx, hz, volume, wave)` is a plain function** outside the
170
- component, taking the context as an argument. The next pass's engine and
171
- scheduler sit beside it in the same shape.
172
- - **Controls are ordinary elements**: named notes as `Button`s, volume as a
173
- plain `<input type="range">`, read at the moment of the tap.
174
- - **The data is real**: the seeded instrument document holds the name, the
175
- wave and the last note; each tap writes a `play` document with `createdAt`,
176
- and a `useLiveQuery` list shows the newest few.
177
- - **What waits for the next pass** is named in the `TODO(next pass)` comment
178
- and the draft note: the sequencer or metronome with lookahead scheduling on
179
- `ctx.currentTime`, effects, recording. The draft schedules nothing on the
180
- clock beyond the one tone.
@@ -1,159 +0,0 @@
1
- # Babylon.js WebXR — the first draft: a lit scene with a door into VR
2
-
3
- A WebXR app's first draft is the page around the scene (title, copy, a card
4
- holding the canvas) with one real Babylon.js scene running inside it: a lit
5
- box turning above a ground plane. The VR half is shown honestly: an "Enter
6
- VR" Button that lights up when the browser reports an immersive headset and
7
- opens the session through `scene.createDefaultXRExperienceAsync`, and reads
8
- "VR needs a headset browser" everywhere else. Hit testing, anchors, hand
9
- tracking, particles and shaders are the next pass. What the draft owes that
10
- pass is the skeleton: the engine and scene are built once in one effect with a
11
- full cleanup, the ground mesh the XR floor needs already exists, and the scene
12
- lives in a ref so the next pass adds features to this file rather than
13
- rewriting it.
14
-
15
- ### `App.jsx`
16
- ```jsx
17
- import React, { useEffect, useRef, useState } from "react";
18
- import { useFireproof } from "use-fireproof";
19
- import * as BABYLON from "@babylonjs/core";
20
- import { Card, CardHeader, CardTitle, CardContent, Button, Badge } from "@vibes.diy/look";
21
-
22
- export default function App() {
23
- const { useLiveQuery } = useFireproof("floatingRoom");
24
- const { docs: rooms } = useLiveQuery("type", { key: "room" });
25
- const room = rooms[0];
26
- const canvasRef = useRef(null);
27
- const sceneRef = useRef(null);
28
- const [vrReady, setVrReady] = useState(false);
29
- const [inVr, setInVr] = useState(false);
30
-
31
- // The engine, built once: scene, camera, lights, ground, one box, render loop, resize, cleanup.
32
- useEffect(() => {
33
- const canvas = canvasRef.current;
34
- const engine = new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true });
35
- const scene = new BABYLON.Scene(engine);
36
- scene.clearColor = new BABYLON.Color4(0.06, 0.08, 0.14, 1);
37
-
38
- const camera = new BABYLON.FreeCamera("cam", new BABYLON.Vector3(0, 1.7, -5), scene);
39
- camera.setTarget(new BABYLON.Vector3(0, 1, 0));
40
- camera.attachControl(canvas, true);
41
-
42
- const ambient = new BABYLON.HemisphericLight("ambient", new BABYLON.Vector3(0, 1, 0), scene);
43
- ambient.intensity = 0.5;
44
- const sun = new BABYLON.DirectionalLight("sun", new BABYLON.Vector3(-1, -2, -1), scene);
45
- sun.position = new BABYLON.Vector3(5, 10, 5);
46
- sun.intensity = 0.8;
47
-
48
- const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 12, height: 12 }, scene);
49
- const groundMat = new BABYLON.StandardMaterial("groundMat", scene);
50
- groundMat.diffuseColor = new BABYLON.Color3(0.22, 0.3, 0.26);
51
- ground.material = groundMat;
52
-
53
- const box = BABYLON.MeshBuilder.CreateBox("box", { size: 1 }, scene);
54
- box.position.y = 1.2;
55
- const boxMat = new BABYLON.StandardMaterial("boxMat", scene);
56
- boxMat.diffuseColor = new BABYLON.Color3(0.95, 0.55, 0.25);
57
- box.material = boxMat;
58
-
59
- scene.onBeforeRenderObservable.add(() => {
60
- const dt = engine.getDeltaTime() / 1000;
61
- box.rotation.y += dt * 0.8;
62
- box.rotation.x += dt * 0.3;
63
- });
64
-
65
- sceneRef.current = { scene, ground };
66
- engine.runRenderLoop(() => scene.render());
67
- const onResize = () => engine.resize();
68
- window.addEventListener("resize", onResize);
69
-
70
- // Ask the browser once whether an immersive headset session is on offer.
71
- let alive = true;
72
- if (navigator.xr) {
73
- navigator.xr
74
- .isSessionSupported("immersive-vr")
75
- .then((ok) => { if (alive) setVrReady(ok); })
76
- .catch(() => { if (alive) setVrReady(false); });
77
- }
78
-
79
- return () => {
80
- alive = false;
81
- sceneRef.current = null;
82
- window.removeEventListener("resize", onResize);
83
- engine.stopRenderLoop();
84
- scene.dispose();
85
- engine.dispose();
86
- };
87
- }, []);
88
-
89
- const enterVr = async () => {
90
- const current = sceneRef.current;
91
- if (!current) return;
92
- const xr = await current.scene.createDefaultXRExperienceAsync({ floorMeshes: [current.ground], disableDefaultUI: true });
93
- xr.baseExperience.onStateChangedObservable.add((state) => setInVr(state === BABYLON.WebXRState.IN_XR));
94
- await xr.baseExperience.enterXRAsync("immersive-vr", "local-floor");
95
- };
96
-
97
- return (
98
- <main className="min-h-screen bg-background text-foreground font-sans p-6">
99
- <h1 className="text-3xl font-bold">{room?.name ?? "Floating Room"}</h1>
100
- <p className="mt-1 text-muted-foreground">{room?.tagline ?? "A quiet room with one bright box. Drag to look around, or step inside with a headset."}</p>
101
-
102
- <Card className="mt-6">
103
- <CardHeader>
104
- <CardTitle>The room</CardTitle>
105
- </CardHeader>
106
- <CardContent>
107
- <canvas ref={canvasRef} className="block aspect-video w-full rounded-lg outline-none touch-none" />
108
- {/* TODO(next pass): hit testing, anchors, hand tracking, particles and shaders inside this same scene */}
109
- <div className="mt-4 flex flex-wrap items-center gap-3">
110
- {vrReady ? (
111
- <Button onClick={enterVr}>Enter VR</Button>
112
- ) : (
113
- <Button disabled variant="outline">VR needs a headset browser</Button>
114
- )}
115
- <Badge variant="secondary">{inVr ? "In VR" : "On screen"}</Badge>
116
- </div>
117
- </CardContent>
118
- </Card>
119
- </main>
120
- );
121
- }
122
- ```
123
-
124
- ### `seed.json`
125
- ```json
126
- {
127
- "floatingRoom": [
128
- { "key": "room", "type": "room", "name": "Floating Room", "tagline": "A quiet room with one bright box. Drag to look around, or step inside with a headset." }
129
- ]
130
- }
131
- ```
132
-
133
- Draft note: the next pass adds hit testing, anchors, hand tracking, particles and shaders to the same scene.
134
-
135
- ## What stays the same in a WebXR draft
136
-
137
- - **The import is `import * as BABYLON from "@babylonjs/core"`**, never the
138
- legacy `babylonjs` package, and every engine class is reached through the
139
- `BABYLON.` namespace.
140
- - **The engine is built once** in `useEffect(() => {...}, [])`: `Engine`,
141
- `Scene`, camera, lights, ground, mesh and `engine.runRenderLoop` all inside
142
- it, per-frame motion in `scene.onBeforeRenderObservable`, and the scene kept
143
- in a ref for the few calls React makes in.
144
- - **The cleanup is complete**: it removes the resize listener, stops the render
145
- loop, disposes the scene and disposes the engine.
146
- - **Sizing**: the canvas is `block aspect-video w-full` inside a Card, and
147
- `window` resize calls `engine.resize()`.
148
- - **The hello is one lit box on a ground plane** under a hemispheric light and
149
- a directional light, turning on the engine's delta time. The ground doubles
150
- as the XR floor mesh.
151
- - **VR is shown, never hidden**: `navigator.xr.isSessionSupported("immersive-vr")`
152
- is checked in the effect and stored in state; a supported browser gets an
153
- "Enter VR" Button that awaits `createDefaultXRExperienceAsync` with
154
- `disableDefaultUI` and then `enterXRAsync("immersive-vr", "local-floor")`,
155
- and every other browser sees a disabled Button reading "VR needs a headset
156
- browser".
157
- - **What waits for the next pass** is named in the `TODO(next pass)` comment
158
- and the draft note: hit testing, anchors, hand tracking, particles, shaders.
159
- The draft draws none of it.