@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.
- package/llms/d3.js +0 -1
- package/llms/d3.js.map +1 -1
- package/llms/fireproof.js +0 -1
- package/llms/fireproof.js.map +1 -1
- package/llms/p5.js +0 -1
- package/llms/p5.js.map +1 -1
- package/llms/spotify.js +0 -1
- package/llms/spotify.js.map +1 -1
- package/llms/three-js.js +0 -1
- package/llms/three-js.js.map +1 -1
- package/llms/types.d.ts +0 -1
- package/llms/voxel.js +0 -1
- package/llms/voxel.js.map +1 -1
- package/llms/web-audio.js +0 -1
- package/llms/web-audio.js.map +1 -1
- package/llms/webxr.js +0 -1
- package/llms/webxr.js.map +1 -1
- package/llms/youtube.js +0 -1
- package/llms/youtube.js.map +1 -1
- package/package.json +4 -4
- package/prompts.d.ts +1 -1
- package/prompts.js +11 -54
- package/prompts.js.map +1 -1
- package/llms/d3.draft.md +0 -154
- package/llms/fireproof.draft.md +0 -47
- package/llms/p5.draft.md +0 -179
- package/llms/spotify.draft.md +0 -179
- package/llms/three-js.draft.md +0 -161
- package/llms/voxel.draft.md +0 -196
- package/llms/web-audio.draft.md +0 -180
- package/llms/webxr.draft.md +0 -159
- package/llms/youtube.draft.md +0 -183
- package/system-prompt-initial-draft.md +0 -121
package/llms/voxel.draft.md
DELETED
|
@@ -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.
|
package/llms/web-audio.draft.md
DELETED
|
@@ -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.
|
package/llms/webxr.draft.md
DELETED
|
@@ -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.
|