@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/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.
@@ -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.
@@ -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.