@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/p5.draft.md
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
# p5.js — the first draft: a small seeded sketch that moves
|
|
2
|
-
|
|
3
|
-
A p5 app's first draft is the whole page around the piece — the title, the
|
|
4
|
-
card that frames it, the button that re-seeds it, the seeded document it reads
|
|
5
|
-
— with one small generative sketch actually running inside the card: a handful
|
|
6
|
-
of dots wandering on Perlin noise, each leaving a soft trail, seeded from the
|
|
7
|
-
document so the same seed always draws the same drift. The real piece (the
|
|
8
|
-
flow field, the kaleidoscope, the thousand-particle study), saving still
|
|
9
|
-
renders, palettes and drawing with a finger are the next pass. What the draft
|
|
10
|
-
owes that pass is the right skeleton: the pinned import, instance mode with a
|
|
11
|
-
sketch factory that receives its host element, the canvas sized from the host,
|
|
12
|
-
`pixelDensity` set, the seed read through a ref, and `instance.remove()` on
|
|
13
|
-
cleanup — so the next pass grows `makeSketch` rather than rewriting the file.
|
|
14
|
-
|
|
15
|
-
### `App.jsx`
|
|
16
|
-
```jsx
|
|
17
|
-
import React, { useEffect, useRef } from "react";
|
|
18
|
-
import { useFireproof } from "use-fireproof";
|
|
19
|
-
import p5 from "https://esm.sh/p5@1.11.13";
|
|
20
|
-
import { Button, Badge, Card, CardHeader, CardTitle, CardContent } from "@vibes.diy/look";
|
|
21
|
-
|
|
22
|
-
const WANDERERS = 7;
|
|
23
|
-
|
|
24
|
-
// Canvas pixels cannot follow a CSS variable, so resolve a theme token to a
|
|
25
|
-
// concrete color once per mount. The fallback covers an app with no theme.
|
|
26
|
-
function cssColor(host, token, fallback) {
|
|
27
|
-
const probe = document.createElement("span");
|
|
28
|
-
probe.style.display = "none";
|
|
29
|
-
probe.style.color = `var(${token}, ${fallback})`;
|
|
30
|
-
host.appendChild(probe);
|
|
31
|
-
const resolved = window.getComputedStyle(probe).color;
|
|
32
|
-
probe.remove();
|
|
33
|
-
return resolved || fallback;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// The sketch is a pure function of its host and the seed it reads. The host is
|
|
37
|
-
// passed in; the current seed arrives through seedRef, so a new seed restarts
|
|
38
|
-
// the drift without rebuilding the instance.
|
|
39
|
-
function makeSketch({ host, seedRef }) {
|
|
40
|
-
return (p) => {
|
|
41
|
-
let w = 0;
|
|
42
|
-
let h = 0;
|
|
43
|
-
let seed = null;
|
|
44
|
-
let t = 0;
|
|
45
|
-
let dots = [];
|
|
46
|
-
let bg = null;
|
|
47
|
-
let fade = null;
|
|
48
|
-
let ink = null;
|
|
49
|
-
|
|
50
|
-
const start = () => {
|
|
51
|
-
seed = seedRef.current;
|
|
52
|
-
p.randomSeed(seed);
|
|
53
|
-
p.noiseSeed(seed);
|
|
54
|
-
t = 0;
|
|
55
|
-
dots = Array.from({ length: WANDERERS }, (_, i) => ({ x: p.random(w), y: p.random(h), r: 6 + i * 2, off: p.random(1000) }));
|
|
56
|
-
p.background(bg);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
p.setup = () => {
|
|
60
|
-
w = Math.max(120, host.clientWidth || 320);
|
|
61
|
-
h = Math.max(120, host.clientHeight || 320);
|
|
62
|
-
const c = p.createCanvas(w, h);
|
|
63
|
-
c.elt.style.display = "block";
|
|
64
|
-
p.pixelDensity(Math.min(window.devicePixelRatio || 1, 2));
|
|
65
|
-
bg = p.color(cssColor(host, "--card", "#fdfaf4"));
|
|
66
|
-
fade = p.color(cssColor(host, "--card", "#fdfaf4"));
|
|
67
|
-
fade.setAlpha(24);
|
|
68
|
-
ink = p.color(cssColor(host, "--primary", "#3b5bdb"));
|
|
69
|
-
ink.setAlpha(170);
|
|
70
|
-
p.noStroke();
|
|
71
|
-
start();
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
p.draw = () => {
|
|
75
|
-
if (seedRef.current !== seed) start(); // the button wrote a new seed
|
|
76
|
-
t += 0.004;
|
|
77
|
-
p.fill(fade); // trails: dim what is there instead of erasing it
|
|
78
|
-
p.rect(0, 0, w, h);
|
|
79
|
-
p.fill(ink);
|
|
80
|
-
for (const d of dots) {
|
|
81
|
-
d.x = p.noise(d.off, t) * w;
|
|
82
|
-
d.y = p.noise(d.off + 50, t) * h;
|
|
83
|
-
p.circle(d.x, d.y, d.r);
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
p.windowResized = () => {
|
|
88
|
-
w = Math.max(120, host.clientWidth);
|
|
89
|
-
h = Math.max(120, host.clientHeight);
|
|
90
|
-
p.resizeCanvas(w, h);
|
|
91
|
-
p.background(bg);
|
|
92
|
-
};
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export default function App() {
|
|
97
|
-
const { database, useLiveQuery } = useFireproof("driftStudio");
|
|
98
|
-
const { docs: pieces } = useLiveQuery("type", { key: "piece" });
|
|
99
|
-
const piece = pieces[0];
|
|
100
|
-
const hostRef = useRef(null);
|
|
101
|
-
const seedRef = useRef(piece?.seed ?? 1234);
|
|
102
|
-
|
|
103
|
-
// The sketch reads the seed from the ref on its next frame.
|
|
104
|
-
useEffect(() => {
|
|
105
|
-
if (piece) seedRef.current = piece.seed;
|
|
106
|
-
}, [piece?.seed]);
|
|
107
|
-
|
|
108
|
-
// The instance, built once into the host element, removed on unmount.
|
|
109
|
-
useEffect(() => {
|
|
110
|
-
const node = hostRef.current;
|
|
111
|
-
if (!node) return undefined;
|
|
112
|
-
const instance = new p5(makeSketch({ host: node, seedRef }), node);
|
|
113
|
-
return () => instance.remove();
|
|
114
|
-
}, []);
|
|
115
|
-
|
|
116
|
-
const reseed = () => {
|
|
117
|
-
if (!piece) return;
|
|
118
|
-
database.put({ ...piece, seed: Math.floor(Math.random() * 100000), reseededAt: Date.now() });
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
return (
|
|
122
|
-
<main className="min-h-screen bg-background text-foreground font-sans p-6">
|
|
123
|
-
<h1 className="text-3xl font-bold">{piece?.title ?? "Drift"}</h1>
|
|
124
|
-
<p className="mt-1 text-muted-foreground">Seven dots wander on Perlin noise. Every seed is its own dance, and the same seed always plays it again.</p>
|
|
125
|
-
|
|
126
|
-
<Card className="mt-6 max-w-xl">
|
|
127
|
-
<CardHeader>
|
|
128
|
-
<CardTitle>{piece?.subtitle ?? "Study no. 1"}</CardTitle>
|
|
129
|
-
</CardHeader>
|
|
130
|
-
<CardContent>
|
|
131
|
-
<div ref={hostRef} className="aspect-square w-full overflow-hidden rounded-lg border border-border touch-none" />
|
|
132
|
-
<div className="mt-4 flex items-center gap-3">
|
|
133
|
-
<Button onClick={reseed}>New seed</Button>
|
|
134
|
-
<Badge variant="secondary">seed {piece?.seed ?? "…"}</Badge>
|
|
135
|
-
</div>
|
|
136
|
-
</CardContent>
|
|
137
|
-
</Card>
|
|
138
|
-
|
|
139
|
-
{/* TODO(next pass): grow makeSketch into the real piece, save still renders of a seed, palette picker, drawing with a finger */}
|
|
140
|
-
</main>
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### `seed.json`
|
|
146
|
-
```json
|
|
147
|
-
{
|
|
148
|
-
"driftStudio": [
|
|
149
|
-
{ "key": "drift", "type": "piece", "title": "Drift", "subtitle": "Study no. 1 — seven wanderers", "seed": 4417 }
|
|
150
|
-
]
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
Draft note: the next pass adds the real generative piece, saving still renders of a favourite seed, a palette picker, and drawing into the sketch with a finger.
|
|
155
|
-
|
|
156
|
-
## What stays the same in a p5 draft
|
|
157
|
-
|
|
158
|
-
- **The import is pinned**: `import p5 from "https://esm.sh/p5@1.11.13"`,
|
|
159
|
-
exactly — never a bare `"p5"`, which resolves to the 2.x line.
|
|
160
|
-
- **Instance mode, built once**: `new p5(makeSketch({ host, ... }), host)`
|
|
161
|
-
inside `useEffect(() => {...}, [])`, and the cleanup returns
|
|
162
|
-
`instance.remove()` so a remount never leaves a second draw loop running.
|
|
163
|
-
Every hook is assigned on `p` (`p.setup`, `p.draw`, `p.windowResized`).
|
|
164
|
-
- **The host is passed in and sizes the canvas**: `createCanvas` reads
|
|
165
|
-
`host.clientWidth`/`clientHeight`, the host is an `aspect-square` (or
|
|
166
|
-
`aspect-video`) box inside a Card, and `p.pixelDensity(Math.min(devicePixelRatio, 2))`
|
|
167
|
-
is set in setup.
|
|
168
|
-
- **The seed lives in a document and reaches the sketch through a ref**:
|
|
169
|
-
the button writes a new `seed` with `database.put`, an effect copies it into
|
|
170
|
-
`seedRef`, and `draw` notices the change and restarts with
|
|
171
|
-
`randomSeed`/`noiseSeed`, so the instance is never rebuilt for a new seed and
|
|
172
|
-
a seed always redraws the same piece.
|
|
173
|
-
- **The colors come from the theme**: `cssColor` resolves `--card` and
|
|
174
|
-
`--primary` into canvas colors; the page itself uses semantic classes.
|
|
175
|
-
- **The hello is small and visibly generative**: a few noise-driven dots with
|
|
176
|
-
a soft trail, never a blank canvas and never a placeholder picture.
|
|
177
|
-
- **What waits for the next pass** is named in the `TODO(next pass)` comment
|
|
178
|
-
and the draft note: the real piece, still renders, palettes, touch drawing.
|
|
179
|
-
The draft draws none of it.
|
package/llms/spotify.draft.md
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
# Spotify catalog — the first draft: search that works on sample tracks
|
|
2
|
-
|
|
3
|
-
A Spotify catalog page's first draft is the finished page, painted from seeded
|
|
4
|
-
`track` documents shaped like the rows the real search route returns: a search
|
|
5
|
-
box and button that filter those tracks right in the browser by track name,
|
|
6
|
-
artist or album, result cards with the fields a real row carries, and the setup
|
|
7
|
-
card that walks the owner through registering a Spotify app. A Badge on the
|
|
8
|
-
header reads "sample data until the key is set", so searching works from the
|
|
9
|
-
first paint and the page says plainly where the live catalog comes from.
|
|
10
|
-
|
|
11
|
-
The next pass writes `backend.js`: it mints a client-credentials token from
|
|
12
|
-
`SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET`, holds it in memory, and
|
|
13
|
-
searches the catalog through the app's own `/_api/search` route. What the draft
|
|
14
|
-
owes that pass is the row shape: each `track` document carries the fields
|
|
15
|
-
`trackRow` returns (`id`, `name`, `artists` as one joined string, `album`,
|
|
16
|
-
`image`, `url`) plus `durationMs`, so the next pass swaps the filter for the
|
|
17
|
-
route and keeps every card. The draft writes no `backend.js`, makes no network
|
|
18
|
-
call, reads no secret, and never asks a visitor for a token: the credentials
|
|
19
|
-
are the owner's, set once in the vibe's settings.
|
|
20
|
-
|
|
21
|
-
### `App.jsx`
|
|
22
|
-
```jsx
|
|
23
|
-
import React, { useState } from "react";
|
|
24
|
-
import { useFireproof } from "use-fireproof";
|
|
25
|
-
import { Button, Card, CardHeader, CardTitle, CardContent, Badge, Input } from "@vibes.diy/look";
|
|
26
|
-
|
|
27
|
-
// The owner's one-time trip to the Spotify developer dashboard, carried in the app.
|
|
28
|
-
const SETUP_STEPS = [
|
|
29
|
-
"Open the Spotify developer dashboard and log in with a Spotify account.",
|
|
30
|
-
"Create an app with any name and description; put this page's URL in if the form asks for a redirect URI.",
|
|
31
|
-
"Open the app's settings and copy the Client ID.",
|
|
32
|
-
"Reveal and copy the Client secret.",
|
|
33
|
-
"Set both on this vibe as SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET in the vibe's settings page.",
|
|
34
|
-
];
|
|
35
|
-
const SECRET_NAMES = ["SPOTIFY_CLIENT_ID", "SPOTIFY_CLIENT_SECRET"];
|
|
36
|
-
|
|
37
|
-
function formatDuration(ms) {
|
|
38
|
-
const total = Math.round(ms / 1000);
|
|
39
|
-
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function matches(track, terms) {
|
|
43
|
-
const haystack = `${track.name} ${track.artists} ${track.album ?? ""}`.toLowerCase();
|
|
44
|
-
return terms.every((t) => haystack.includes(t));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function TrackCard({ track }) {
|
|
48
|
-
return (
|
|
49
|
-
<Card>
|
|
50
|
-
<CardContent>
|
|
51
|
-
<div className="flex items-center gap-4">
|
|
52
|
-
{track.image ? (
|
|
53
|
-
<img src={track.image} alt="" className="h-16 w-16 rounded-md object-cover" />
|
|
54
|
-
) : (
|
|
55
|
-
<div className="flex h-16 w-16 items-center justify-center rounded-md bg-muted text-2xl font-bold text-muted-foreground">
|
|
56
|
-
{track.name.charAt(0)}
|
|
57
|
-
</div>
|
|
58
|
-
)}
|
|
59
|
-
<div className="min-w-0 flex-1">
|
|
60
|
-
<p className="truncate font-semibold">
|
|
61
|
-
{track.url ? <a href={track.url} target="_blank" rel="noreferrer" className="underline">{track.name}</a> : track.name}
|
|
62
|
-
</p>
|
|
63
|
-
<p className="truncate text-muted-foreground">{track.artists}</p>
|
|
64
|
-
<p className="truncate text-muted-foreground">{track.album}</p>
|
|
65
|
-
</div>
|
|
66
|
-
<span className="tabular-nums text-muted-foreground">{formatDuration(track.durationMs)}</span>
|
|
67
|
-
</div>
|
|
68
|
-
</CardContent>
|
|
69
|
-
</Card>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export default function App() {
|
|
74
|
-
const { useLiveQuery } = useFireproof("spotifyCatalog");
|
|
75
|
-
const { docs: tracks } = useLiveQuery("type", { key: "track" });
|
|
76
|
-
const [query, setQuery] = useState("");
|
|
77
|
-
const [applied, setApplied] = useState("");
|
|
78
|
-
|
|
79
|
-
const terms = applied.toLowerCase().split(/\s+/).filter(Boolean);
|
|
80
|
-
const results = terms.length === 0 ? tracks : tracks.filter((t) => matches(t, terms));
|
|
81
|
-
const search = () => setApplied(query.trim());
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<main className="min-h-screen bg-background text-foreground font-sans p-6">
|
|
85
|
-
<div className="flex flex-wrap items-center gap-3">
|
|
86
|
-
<h1 className="text-3xl font-bold">Crate Digger</h1>
|
|
87
|
-
<Badge variant="secondary">sample data until the key is set</Badge>
|
|
88
|
-
</div>
|
|
89
|
-
<p className="mt-1 text-muted-foreground">Search Spotify's catalog by song, artist or album.</p>
|
|
90
|
-
|
|
91
|
-
<form
|
|
92
|
-
className="mt-6 flex gap-2"
|
|
93
|
-
onSubmit={(e) => {
|
|
94
|
-
e.preventDefault();
|
|
95
|
-
search();
|
|
96
|
-
}}
|
|
97
|
-
>
|
|
98
|
-
<Input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Try “harbor” or “Juniper Static”" />
|
|
99
|
-
<Button type="submit">Search</Button>
|
|
100
|
-
</form>
|
|
101
|
-
{/* TODO(next pass): the server handler mints the client-credentials token from SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET and searches the catalog through /_api/search; these cards render its rows */}
|
|
102
|
-
|
|
103
|
-
<p className="mt-4 text-muted-foreground">
|
|
104
|
-
{applied ? `${results.length} tracks for “${applied}”` : `${results.length} tracks to explore`}
|
|
105
|
-
</p>
|
|
106
|
-
<div className="mt-3 grid gap-3">
|
|
107
|
-
{results.map((track) => (
|
|
108
|
-
<TrackCard key={track.id} track={track} />
|
|
109
|
-
))}
|
|
110
|
-
</div>
|
|
111
|
-
{applied && results.length === 0 && (
|
|
112
|
-
<p className="mt-3 text-muted-foreground">Try a different word from a song, artist or album.</p>
|
|
113
|
-
)}
|
|
114
|
-
|
|
115
|
-
<Card className="mt-8">
|
|
116
|
-
<CardHeader>
|
|
117
|
-
<CardTitle>Search the whole Spotify catalog</CardTitle>
|
|
118
|
-
</CardHeader>
|
|
119
|
-
<CardContent>
|
|
120
|
-
<p className="text-muted-foreground">Register a Spotify app once, and this search reaches every track on Spotify.</p>
|
|
121
|
-
<ol className="mt-3 grid list-decimal gap-2 pl-5">
|
|
122
|
-
{SETUP_STEPS.map((step) => (
|
|
123
|
-
<li key={step}>{step}</li>
|
|
124
|
-
))}
|
|
125
|
-
</ol>
|
|
126
|
-
<p className="mt-3">
|
|
127
|
-
Both secrets, {SECRET_NAMES.map((name, i) => (
|
|
128
|
-
<span key={name}>
|
|
129
|
-
{i > 0 && " and "}
|
|
130
|
-
<code className="rounded bg-muted px-1">{name}</code>
|
|
131
|
-
</span>
|
|
132
|
-
))}, stay on the server.
|
|
133
|
-
</p>
|
|
134
|
-
</CardContent>
|
|
135
|
-
</Card>
|
|
136
|
-
</main>
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### `seed.json`
|
|
142
|
-
```json
|
|
143
|
-
{
|
|
144
|
-
"spotifyCatalog": [
|
|
145
|
-
{ "key": "t1", "type": "track", "id": "4kXq0sample1Harbor", "name": "Harbor Lights at Low Tide", "artists": "Juniper Static", "album": "Saltwater Radio", "image": null, "url": null, "durationMs": 214000 },
|
|
146
|
-
{ "key": "t2", "type": "track", "id": "7bRt2sample2Tangerine", "name": "Tangerine Freeway", "artists": "The Velvet Hours, Mara Quill", "album": "Summer in Reverse", "image": null, "url": null, "durationMs": 187500 },
|
|
147
|
-
{ "key": "t3", "type": "track", "id": "2mPz9sample3Lantern", "name": "Lantern Song", "artists": "Juniper Static", "album": "Saltwater Radio", "image": null, "url": null, "durationMs": 243800 },
|
|
148
|
-
{ "key": "t4", "type": "track", "id": "9cWe4sample4Paper", "name": "Paper Planets", "artists": "Odessa Bloom", "album": "Small Weather", "image": null, "url": null, "durationMs": 198200 },
|
|
149
|
-
{ "key": "t5", "type": "track", "id": "1hVn6sample5Midnight", "name": "Midnight Laundromat", "artists": "Kit Arden", "album": "Quarters", "image": null, "url": null, "durationMs": 226400 }
|
|
150
|
-
]
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
Draft note: the next pass adds backend.js, which mints the client-credentials token from `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` and searches the public catalog, so the same cards show live tracks with album art and Spotify links.
|
|
155
|
-
|
|
156
|
-
## What stays the same in a Spotify draft
|
|
157
|
-
|
|
158
|
-
- **The whole page paints on the first draft**: header, search, result cards
|
|
159
|
-
and the setup card. Searching already works, filtering the seeded tracks in
|
|
160
|
-
the browser by track name, artist and album.
|
|
161
|
-
- **Seeded rows mirror the backend's rows**: each `track` document carries
|
|
162
|
-
`trackRow`'s fields (`id`, `name`, `artists` joined into one string, `album`,
|
|
163
|
-
`image`, `url`) plus `durationMs`, so the next pass swaps the source and keeps
|
|
164
|
-
every card. A card handles a missing `image` or `url` gracefully.
|
|
165
|
-
- **The capability is labeled, never hidden**: a Badge on the header reads
|
|
166
|
-
"sample data until the key is set", and the setup card names the Spotify
|
|
167
|
-
developer dashboard, `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` in so
|
|
168
|
-
many words.
|
|
169
|
-
- **The credentials are the owner's**: the setup steps are for the owner, once.
|
|
170
|
-
No input asks a visitor for a token, and no visitor is sent to a developer
|
|
171
|
-
console.
|
|
172
|
-
- **The page reads the public catalog**: search, artists, albums and tracks. A
|
|
173
|
-
listener's own top tracks, playlists and saved songs belong to their own
|
|
174
|
-
Spotify account, which is a connection and not this flow, so the draft draws
|
|
175
|
-
no section for them.
|
|
176
|
-
- **What waits for the next pass** is named in the `TODO(next pass)` comment and
|
|
177
|
-
the draft note: `backend.js`, the client-credentials token held in memory, the
|
|
178
|
-
`/_api/search` route and its refusal reasons. The draft writes no
|
|
179
|
-
`backend.js`, makes no network call and reads no secret.
|
package/llms/three-js.draft.md
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
# Three.js — the first draft: one lit object turning in a viewport
|
|
2
|
-
|
|
3
|
-
A Three.js app's first draft is the whole page around the scene (title, copy,
|
|
4
|
-
a card holding the viewport, a control row) with one real, lit object turning
|
|
5
|
-
inside it. The seeded document names the scene and holds its color, and the
|
|
6
|
-
control row repaints the object live. Loading models, physics, particles,
|
|
7
|
-
camera controls and textures are the next pass. What the draft owes that pass
|
|
8
|
-
is the platform's own engine skeleton: the renderer, scene, camera, lights and
|
|
9
|
-
loop are built once inside one effect, sized to their host, and fully disposed
|
|
10
|
-
on unmount, and React talks to the running scene through a ref, so the next
|
|
11
|
-
pass adds meshes and systems to this file rather than rewriting it.
|
|
12
|
-
|
|
13
|
-
### `App.jsx`
|
|
14
|
-
```jsx
|
|
15
|
-
import React, { useEffect, useRef } from "react";
|
|
16
|
-
import { useFireproof } from "use-fireproof";
|
|
17
|
-
import * as THREE from "three";
|
|
18
|
-
import { Card, CardHeader, CardTitle, CardContent, Button, Badge } from "@vibes.diy/look";
|
|
19
|
-
|
|
20
|
-
// The finishes the control row offers, named once for the buttons and the material.
|
|
21
|
-
const FINISHES = [
|
|
22
|
-
{ label: "Lagoon", color: "#3fa7d6" },
|
|
23
|
-
{ label: "Marigold", color: "#f4b942" },
|
|
24
|
-
{ label: "Orchid", color: "#c86bd8" },
|
|
25
|
-
{ label: "Moss", color: "#6fae5b" },
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
export default function App() {
|
|
29
|
-
const { database, useLiveQuery } = useFireproof("spinStudio");
|
|
30
|
-
const { docs: scenes } = useLiveQuery("type", { key: "scene" });
|
|
31
|
-
const scene = scenes[0];
|
|
32
|
-
const hostRef = useRef(null);
|
|
33
|
-
// The running engine reads the color from here each frame, so a change never rebuilds it.
|
|
34
|
-
const colorRef = useRef(scene?.color ?? FINISHES[0].color);
|
|
35
|
-
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
if (scene?.color) colorRef.current = scene.color;
|
|
38
|
-
}, [scene?.color]);
|
|
39
|
-
|
|
40
|
-
// The engine, built once: renderer, scene, camera, lights, one mesh, loop, resize, cleanup.
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
const host = hostRef.current;
|
|
43
|
-
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
44
|
-
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
45
|
-
renderer.setSize(host.clientWidth, host.clientHeight);
|
|
46
|
-
host.appendChild(renderer.domElement);
|
|
47
|
-
|
|
48
|
-
const world = new THREE.Scene();
|
|
49
|
-
const camera = new THREE.PerspectiveCamera(50, host.clientWidth / host.clientHeight, 0.1, 100);
|
|
50
|
-
camera.position.set(0, 1.2, 4.2);
|
|
51
|
-
camera.lookAt(0, 0, 0);
|
|
52
|
-
|
|
53
|
-
world.add(new THREE.HemisphereLight(0xffffff, 0x445066, 0.8));
|
|
54
|
-
const sun = new THREE.DirectionalLight(0xffffff, 1.4);
|
|
55
|
-
sun.position.set(3, 5, 2);
|
|
56
|
-
world.add(sun);
|
|
57
|
-
|
|
58
|
-
const geometry = new THREE.BoxGeometry(1.4, 1.4, 1.4);
|
|
59
|
-
const material = new THREE.MeshStandardMaterial({ color: colorRef.current, roughness: 0.35, metalness: 0.15 });
|
|
60
|
-
const mesh = new THREE.Mesh(geometry, material);
|
|
61
|
-
world.add(mesh);
|
|
62
|
-
|
|
63
|
-
const onResize = () => {
|
|
64
|
-
const w = host.clientWidth;
|
|
65
|
-
const h = host.clientHeight;
|
|
66
|
-
if (w === 0 || h === 0) return;
|
|
67
|
-
renderer.setSize(w, h);
|
|
68
|
-
camera.aspect = w / h;
|
|
69
|
-
camera.updateProjectionMatrix();
|
|
70
|
-
};
|
|
71
|
-
const observer = new window.ResizeObserver(onResize); // follows the host, not just the window
|
|
72
|
-
observer.observe(host);
|
|
73
|
-
|
|
74
|
-
const clock = new THREE.Clock();
|
|
75
|
-
let raf = 0;
|
|
76
|
-
const animate = () => {
|
|
77
|
-
const dt = Math.min(clock.getDelta(), 0.05);
|
|
78
|
-
mesh.rotation.x += dt * 0.5;
|
|
79
|
-
mesh.rotation.y += dt * 0.8;
|
|
80
|
-
material.color.set(colorRef.current);
|
|
81
|
-
renderer.render(world, camera);
|
|
82
|
-
raf = requestAnimationFrame(animate);
|
|
83
|
-
};
|
|
84
|
-
animate();
|
|
85
|
-
|
|
86
|
-
return () => {
|
|
87
|
-
cancelAnimationFrame(raf);
|
|
88
|
-
observer.disconnect();
|
|
89
|
-
geometry.dispose();
|
|
90
|
-
material.dispose();
|
|
91
|
-
renderer.dispose();
|
|
92
|
-
if (renderer.domElement.parentNode === host) host.removeChild(renderer.domElement);
|
|
93
|
-
};
|
|
94
|
-
}, []);
|
|
95
|
-
|
|
96
|
-
const paint = (color) => {
|
|
97
|
-
colorRef.current = color;
|
|
98
|
-
if (scene) database.put({ ...scene, color });
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<main className="min-h-screen bg-background text-foreground font-sans p-6">
|
|
103
|
-
<h1 className="text-3xl font-bold">{scene?.name ?? "Spin Studio"}</h1>
|
|
104
|
-
<p className="mt-1 text-muted-foreground">A little object on a turntable. Pick a finish and watch it catch the light.</p>
|
|
105
|
-
|
|
106
|
-
<Card className="mt-6">
|
|
107
|
-
<CardHeader>
|
|
108
|
-
<CardTitle>Turntable</CardTitle>
|
|
109
|
-
</CardHeader>
|
|
110
|
-
<CardContent>
|
|
111
|
-
<div ref={hostRef} className="aspect-video w-full rounded-lg overflow-hidden bg-muted" />
|
|
112
|
-
{/* TODO(next pass): load a GLTF model, orbit controls, textures, particles and physics into this same scene */}
|
|
113
|
-
<div className="mt-4 flex flex-wrap items-center gap-2">
|
|
114
|
-
{FINISHES.map((f) => (
|
|
115
|
-
<Button key={f.label} size="sm" variant={f.color === scene?.color ? "default" : "outline"} onClick={() => paint(f.color)}>
|
|
116
|
-
{f.label}
|
|
117
|
-
</Button>
|
|
118
|
-
))}
|
|
119
|
-
<Badge variant="secondary" className="ml-auto">{FINISHES.find((f) => f.color === scene?.color)?.label ?? "Lagoon"}</Badge>
|
|
120
|
-
</div>
|
|
121
|
-
</CardContent>
|
|
122
|
-
</Card>
|
|
123
|
-
</main>
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### `seed.json`
|
|
129
|
-
```json
|
|
130
|
-
{
|
|
131
|
-
"spinStudio": [
|
|
132
|
-
{ "key": "turntable", "type": "scene", "name": "Spin Studio", "color": "#3fa7d6" }
|
|
133
|
-
]
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Draft note: the next pass adds loaded models, orbit controls, textures, particles and physics to the same scene.
|
|
138
|
-
|
|
139
|
-
## What stays the same in a Three.js draft
|
|
140
|
-
|
|
141
|
-
- **The engine is built once** in `useEffect(() => {...}, [])`: renderer,
|
|
142
|
-
scene, camera, lights, mesh and `requestAnimationFrame` loop all inside it.
|
|
143
|
-
React never rebuilds it; values React changes reach the loop through a ref
|
|
144
|
-
(`colorRef`), never through the effect's dependency list.
|
|
145
|
-
- **The cleanup is complete**: it cancels the frame, disconnects the resize
|
|
146
|
-
observer, disposes every geometry and material it made, disposes the
|
|
147
|
-
renderer, and removes the canvas from its host.
|
|
148
|
-
- **The viewport sizes to its host**, not the window: a `div` with a fixed
|
|
149
|
-
aspect (`aspect-video w-full rounded-lg overflow-hidden`) inside a Card, a
|
|
150
|
-
resize handler that updates the renderer size and the camera aspect, and
|
|
151
|
-
`setPixelRatio(Math.min(window.devicePixelRatio, 2))`.
|
|
152
|
-
- **The hello is one lit object**: a `BoxGeometry` with a
|
|
153
|
-
`MeshStandardMaterial` under a hemisphere light and a directional light,
|
|
154
|
-
turning on a clock delta. Standard materials need light; the draft always
|
|
155
|
-
ships both.
|
|
156
|
-
- **The page is complete around it**: title, a sentence of friendly copy, the
|
|
157
|
-
Card, and a control row of Buttons whose choice saves to the seeded
|
|
158
|
-
Fireproof document and shows up in the scene on the next frame.
|
|
159
|
-
- **What waits for the next pass** is named in the `TODO(next pass)` comment
|
|
160
|
-
and the draft note: models, controls, textures, particles, physics. The
|
|
161
|
-
draft draws none of it and imports nothing beyond `three` itself.
|