@vibes.diy/prompts 14.1.20 → 14.1.22
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/access.md +83 -4
- package/llms/backend.md +27 -0
- package/llms/callai.initial.md +121 -0
- package/llms/callai.js +1 -0
- package/llms/callai.js.map +1 -1
- package/llms/debugging.md +114 -0
- package/llms/fireproof.initial.md +697 -0
- package/llms/fireproof.js +1 -0
- package/llms/fireproof.js.map +1 -1
- package/llms/image-gen.initial.md +155 -0
- package/llms/image-gen.js +1 -0
- package/llms/image-gen.js.map +1 -1
- package/llms/index.d.ts +2 -1
- package/llms/index.js +3 -0
- package/llms/index.js.map +1 -1
- package/llms/p5.d.ts +2 -0
- package/llms/p5.js +28 -0
- package/llms/p5.js.map +1 -0
- package/llms/p5.md +714 -0
- package/llms/types.d.ts +1 -0
- package/llms/use-vibe.initial.md +58 -0
- package/llms/use-vibe.js +1 -0
- package/llms/use-vibe.js.map +1 -1
- package/llms/use-viewer.initial.md +303 -0
- package/llms/use-viewer.js +1 -0
- package/llms/use-viewer.js.map +1 -1
- package/llms/use-viewer.md +20 -0
- package/package.json +4 -4
- package/prompts.js +4 -3
- package/prompts.js.map +1 -1
- package/system-prompt-initial-oneshot.md +3 -1
- package/system-prompt-initial.md +3 -1
- package/system-prompt.md +3 -1
package/llms/p5.md
ADDED
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
# p5.js API
|
|
2
|
+
|
|
3
|
+
_Instance mode, the pinned import, determinism, and the drawing surface for generative sketches_
|
|
4
|
+
|
|
5
|
+
Any request for generative art, a flow field, a particle field, a screensaver, a
|
|
6
|
+
visualiser, a doodle pad, a drawing toy, a kaleidoscope, a plotter-style pattern
|
|
7
|
+
or "a sketch" is a p5.js app: one canvas created by a p5 **instance**, driven by
|
|
8
|
+
a `setup`/`draw` pair, mounted from a single `React.useEffect(() => {…}, [])`
|
|
9
|
+
and torn down with `instance.remove()` on unmount. p5 gives you a canvas, a
|
|
10
|
+
frame loop, Perlin noise and a seeded random generator for free.
|
|
11
|
+
|
|
12
|
+
The whole model of p5 is two functions: `setup` runs once, `draw` runs about
|
|
13
|
+
sixty times a second, forever. A still picture is a loop that draws the same
|
|
14
|
+
thing every frame; an animation is a loop where something changed.
|
|
15
|
+
|
|
16
|
+
## Complete starter — the shell every p5 vibe begins from
|
|
17
|
+
|
|
18
|
+
This is a full, runnable `App.jsx`. Start here and grow it: the sketch is a
|
|
19
|
+
factory function that receives the host element, the instance is built once in
|
|
20
|
+
one `useEffect([])`, the canvas is sized from the host, and the cleanup removes
|
|
21
|
+
the instance so remounts do not leak a second draw loop.
|
|
22
|
+
|
|
23
|
+
App.jsx
|
|
24
|
+
|
|
25
|
+
```jsx
|
|
26
|
+
import React from "react";
|
|
27
|
+
import p5 from "https://esm.sh/p5@1.11.13";
|
|
28
|
+
|
|
29
|
+
const TAU = Math.PI * 2;
|
|
30
|
+
|
|
31
|
+
// Canvas pixels cannot follow a CSS variable, so resolve the injected theme
|
|
32
|
+
// tokens to concrete colors once per mount. Setting `color` on a throwaway
|
|
33
|
+
// element and reading it back returns a resolved `rgb(...)` string even when the
|
|
34
|
+
// token's value is a `color-mix()` or another `var()`, and the second argument
|
|
35
|
+
// is the fallback for an app with no theme injected.
|
|
36
|
+
function cssColor(host, token, fallback) {
|
|
37
|
+
const probe = document.createElement("span");
|
|
38
|
+
probe.style.display = "none";
|
|
39
|
+
probe.style.color = `var(${token}, ${fallback})`;
|
|
40
|
+
host.appendChild(probe);
|
|
41
|
+
const resolved = window.getComputedStyle(probe).color;
|
|
42
|
+
probe.remove();
|
|
43
|
+
return resolved || fallback;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The sketch is a pure function of its parameters and its host element. Pass the
|
|
47
|
+
// host in explicitly — never read p5's private `_userNode` to find it.
|
|
48
|
+
function makeSketch({ params, host, stepsRef }) {
|
|
49
|
+
return (p) => {
|
|
50
|
+
let particles = [];
|
|
51
|
+
let palette = null;
|
|
52
|
+
let steps = 0;
|
|
53
|
+
let w = 0;
|
|
54
|
+
let h = 0;
|
|
55
|
+
let t = 0;
|
|
56
|
+
|
|
57
|
+
// Read the theme tokens into p5 colors. Called at setup AND whenever the
|
|
58
|
+
// page's CSS variables change under a live restyle.
|
|
59
|
+
const readPalette = () => {
|
|
60
|
+
const bg = p.color(cssColor(host, "--background", "#040a18"));
|
|
61
|
+
const ink = p.color(cssColor(host, "--accent", "#00e5ff"));
|
|
62
|
+
const fade = p.color(cssColor(host, "--background", "#040a18"));
|
|
63
|
+
fade.setAlpha(10);
|
|
64
|
+
ink.setAlpha(85);
|
|
65
|
+
palette = { bg, ink, fade };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// The step count lives inside this closure, and the Keep button lives in
|
|
69
|
+
// React — so publish it outward on every step. `p` exists ONLY here.
|
|
70
|
+
const publishSteps = () => {
|
|
71
|
+
if (stepsRef) stepsRef.current = steps;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const spawn = () => {
|
|
75
|
+
particles = [];
|
|
76
|
+
for (let i = 0; i < params.count; i++) {
|
|
77
|
+
const x = p.random(w);
|
|
78
|
+
const y = p.random(h);
|
|
79
|
+
particles.push({ x, y, px: x, py: y });
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const advance = () => {
|
|
84
|
+
steps += 1;
|
|
85
|
+
t += params.speed * 0.0016;
|
|
86
|
+
for (const q of particles) {
|
|
87
|
+
const a = p.noise(q.x * params.grain, q.y * params.grain, t) * TAU * 2;
|
|
88
|
+
q.px = q.x;
|
|
89
|
+
q.py = q.y;
|
|
90
|
+
q.x += Math.cos(a) * params.speed;
|
|
91
|
+
q.y += Math.sin(a) * params.speed;
|
|
92
|
+
p.line(q.px, q.py, q.x, q.y);
|
|
93
|
+
if (q.x < -20 || q.x > w + 20 || q.y < -20 || q.y > h + 20) {
|
|
94
|
+
q.x = p.random(w);
|
|
95
|
+
q.y = p.random(h);
|
|
96
|
+
q.px = q.x;
|
|
97
|
+
q.py = q.y;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
p.setup = () => {
|
|
103
|
+
w = Math.max(120, host.clientWidth || 320);
|
|
104
|
+
h = Math.max(120, host.clientHeight || 240);
|
|
105
|
+
const c = p.createCanvas(w, h);
|
|
106
|
+
if (c && c.elt) c.elt.style.display = "block";
|
|
107
|
+
p.pixelDensity(Math.min(window.devicePixelRatio || 1, 2));
|
|
108
|
+
p.randomSeed(params.seed);
|
|
109
|
+
p.noiseSeed(params.seed);
|
|
110
|
+
|
|
111
|
+
readPalette();
|
|
112
|
+
p.stroke(palette.ink);
|
|
113
|
+
p.strokeWeight(1);
|
|
114
|
+
p.background(palette.bg);
|
|
115
|
+
spawn();
|
|
116
|
+
publishSteps();
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// ONE frame of the piece: the fade is part of the frame, not decoration
|
|
120
|
+
// around it, so the still render below replays this and not just advance().
|
|
121
|
+
const step = () => {
|
|
122
|
+
// Trails: dim what is already there instead of erasing it.
|
|
123
|
+
p.noStroke();
|
|
124
|
+
p.fill(palette.fade);
|
|
125
|
+
p.rect(0, 0, w, h);
|
|
126
|
+
p.stroke(palette.ink);
|
|
127
|
+
advance();
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
p.draw = () => {
|
|
131
|
+
step();
|
|
132
|
+
publishSteps();
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// A live restyle swaps the page's CSS variables in place; the canvas is
|
|
136
|
+
// painted, so it has to be told. Called by the observer in App below.
|
|
137
|
+
p.retheme = () => {
|
|
138
|
+
if (palette === null) return;
|
|
139
|
+
readPalette();
|
|
140
|
+
p.stroke(palette.ink);
|
|
141
|
+
p.background(palette.bg);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
p.mouseDragged = () => {
|
|
145
|
+
particles.push({ x: p.mouseX, y: p.mouseY, px: p.mouseX, py: p.mouseY });
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
p.touchMoved = () => {
|
|
149
|
+
particles.push({ x: p.mouseX, y: p.mouseY, px: p.mouseX, py: p.mouseY });
|
|
150
|
+
return false; // stop the browser scrolling the page under the finger
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
p.windowResized = () => {
|
|
154
|
+
w = Math.max(120, host.clientWidth);
|
|
155
|
+
h = Math.max(120, host.clientHeight);
|
|
156
|
+
p.resizeCanvas(w, h);
|
|
157
|
+
p.background(palette.bg);
|
|
158
|
+
spawn();
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default function App() {
|
|
164
|
+
const hostRef = React.useRef(null);
|
|
165
|
+
const stepsRef = React.useRef(0);
|
|
166
|
+
const [params, setParams] = React.useState({
|
|
167
|
+
count: 1200,
|
|
168
|
+
grain: 0.0022,
|
|
169
|
+
speed: 1.6,
|
|
170
|
+
seed: 1234,
|
|
171
|
+
});
|
|
172
|
+
const key = JSON.stringify(params);
|
|
173
|
+
|
|
174
|
+
React.useEffect(() => {
|
|
175
|
+
const node = hostRef.current;
|
|
176
|
+
if (!node) return undefined;
|
|
177
|
+
const instance = new p5(makeSketch({ params, host: node, stepsRef }), node);
|
|
178
|
+
|
|
179
|
+
// The Style tab recolors a running app by swapping the CSS variables with
|
|
180
|
+
// no remount, which every styled element follows and the canvas cannot.
|
|
181
|
+
// Watching the document's stylesheets is what makes the sketch follow too.
|
|
182
|
+
const observer = new MutationObserver(() => instance.retheme());
|
|
183
|
+
observer.observe(document.head, { childList: true, subtree: true, characterData: true });
|
|
184
|
+
|
|
185
|
+
return () => {
|
|
186
|
+
observer.disconnect();
|
|
187
|
+
instance.remove();
|
|
188
|
+
};
|
|
189
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
190
|
+
}, [key]);
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<div className="min-h-screen bg-[var(--background)] text-[var(--text-primary)] p-4">
|
|
194
|
+
<div
|
|
195
|
+
ref={hostRef}
|
|
196
|
+
className="w-full h-[70vh] overflow-hidden rounded-[var(--radius)] border border-[var(--border)]"
|
|
197
|
+
style={{ touchAction: "none" }}
|
|
198
|
+
/>
|
|
199
|
+
<button
|
|
200
|
+
className="mt-3 rounded-full border border-[var(--border)] px-4 py-2 text-sm"
|
|
201
|
+
onClick={() => setParams((prev) => ({ ...prev, seed: Math.floor(Math.random() * 100000) }))}
|
|
202
|
+
>
|
|
203
|
+
Reseed
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Everything below grows that shell. The lifecycle above — one instance, one
|
|
211
|
+
`useEffect`, `remove()` on cleanup — stays the spine of every p5 vibe.
|
|
212
|
+
|
|
213
|
+
## Instance mode is how p5 runs in a vibe
|
|
214
|
+
|
|
215
|
+
Every p5 tutorial on the internet opens with a global `function setup()`. That
|
|
216
|
+
works when the sketch owns the whole page. In a vibe React owns the page, and
|
|
217
|
+
the sketch is a component that mounts, unmounts, and mounts again every time an
|
|
218
|
+
input changes — so the sketch must be an **instance**.
|
|
219
|
+
|
|
220
|
+
**Create the instance inside one `React.useEffect` and return `() => instance.remove()` from it.**
|
|
221
|
+
Without the cleanup, every change to the sketch's inputs mounts a fresh instance
|
|
222
|
+
beside the old one and the old one keeps running: two draw loops, then four, a
|
|
223
|
+
leaked canvas per change, and a sketch that gets mysteriously slower and
|
|
224
|
+
blurrier every time a slider moves.
|
|
225
|
+
|
|
226
|
+
**Assign every lifecycle hook on the instance** — `p.setup`, `p.draw`,
|
|
227
|
+
`p.windowResized`, `p.mouseDragged` — rather than declaring them as globals.
|
|
228
|
+
The instance is the only handle the sketch has, so everything it draws with goes
|
|
229
|
+
through `p`: `p.noise`, `p.random`, `p.line`, `p.fill`.
|
|
230
|
+
|
|
231
|
+
**Pass the host element into the sketch factory explicitly.** `new p5(sketch, host)`
|
|
232
|
+
draws into that element instead of appending a canvas to the body, which is what
|
|
233
|
+
keeps the layout React's and the pixels p5's. p5 also stashes the node on the
|
|
234
|
+
instance, but that property is private — hand the node to your factory as an
|
|
235
|
+
argument instead of reading `_userNode` back off `p`.
|
|
236
|
+
|
|
237
|
+
```jsx
|
|
238
|
+
const hostRef = React.useRef(null);
|
|
239
|
+
|
|
240
|
+
React.useEffect(() => {
|
|
241
|
+
const node = hostRef.current;
|
|
242
|
+
if (!node) return undefined;
|
|
243
|
+
const instance = new p5(makeSketch({ params, host: node }), node);
|
|
244
|
+
return () => instance.remove();
|
|
245
|
+
}, [key]);
|
|
246
|
+
|
|
247
|
+
return <div ref={hostRef} className="w-full h-full" style={{ touchAction: "none" }} />;
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Because the effect re-runs on a changed `key`, a parameter change is a clean
|
|
251
|
+
swap: the old instance is removed, a new one is built, and the sketch stays a
|
|
252
|
+
pure function of its parameters.
|
|
253
|
+
|
|
254
|
+
## Importing p5
|
|
255
|
+
|
|
256
|
+
**Import p5 from a version-pinned esm.sh URL: `import p5 from "https://esm.sh/p5@1.11.13"`.**
|
|
257
|
+
A bare `"p5"` resolves to the 2.x line, which pulls a much heavier module graph
|
|
258
|
+
(acorn, escodegen) into the sandbox for an API this doc never uses.
|
|
259
|
+
|
|
260
|
+
Pinning is a correctness property here, not caution. For generative art a
|
|
261
|
+
library bump can change what a piece *looks like* — a tweak to `noise()`, a
|
|
262
|
+
different default `pixelDensity` — and a saved piece is a handful of numbers
|
|
263
|
+
that assume the renderer they were saved on. Pin the version the way you would
|
|
264
|
+
date a print.
|
|
265
|
+
|
|
266
|
+
## Sizing the canvas
|
|
267
|
+
|
|
268
|
+
Size from the host element, never from `window.innerWidth`: the sketch lives in
|
|
269
|
+
a box React laid out, and the box is rarely the whole window.
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
p.setup = () => {
|
|
273
|
+
w = Math.max(120, host.clientWidth || 320);
|
|
274
|
+
h = Math.max(120, host.clientHeight || 240);
|
|
275
|
+
p.createCanvas(w, h);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
p.windowResized = () => {
|
|
279
|
+
w = Math.max(120, host.clientWidth);
|
|
280
|
+
h = Math.max(120, host.clientHeight);
|
|
281
|
+
p.resizeCanvas(w, h);
|
|
282
|
+
};
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
`p.resizeCanvas` clears the canvas, so a trails sketch repaints its background
|
|
286
|
+
and respawns its particles after a resize rather than resuming into an empty
|
|
287
|
+
frame.
|
|
288
|
+
|
|
289
|
+
**Cap pixel density at 2 for a live canvas and set `p.pixelDensity(1)` for thumbnails.**
|
|
290
|
+
On a retina screen the cost of a frame is fill rate, not JavaScript, and a
|
|
291
|
+
gallery of small cards at device density is the fastest way to make a page
|
|
292
|
+
stutter.
|
|
293
|
+
|
|
294
|
+
## Colour comes from the injected theme tokens
|
|
295
|
+
|
|
296
|
+
The platform injects this app's colours as CSS variables — `--background`,
|
|
297
|
+
`--surface`, `--primary`, `--secondary`, `--accent`, `--text-primary`,
|
|
298
|
+
`--text-secondary`, `--border` — and the owner restyles the whole app by swapping
|
|
299
|
+
those values. Everything outside the canvas reads them through Tailwind bracket
|
|
300
|
+
notation (`bg-[var(--background)]`), but **canvas pixels cannot follow a CSS
|
|
301
|
+
variable**, so a sketch that inlines `"#00e5ff"` is the one part of a themed vibe
|
|
302
|
+
its owner can never restyle.
|
|
303
|
+
|
|
304
|
+
**Resolve the theme tokens to concrete colours at setup, and use a literal only
|
|
305
|
+
as the fallback when no theme is injected.** Reading the custom property
|
|
306
|
+
straight off the host with `getComputedStyle(host).getPropertyValue("--accent")`
|
|
307
|
+
is enough when the token holds a plain colour, but a token whose value is a
|
|
308
|
+
`color-mix()` or another `var()` comes back unresolved and p5 cannot parse it.
|
|
309
|
+
Letting the browser resolve it works for every value:
|
|
310
|
+
|
|
311
|
+
```js
|
|
312
|
+
function cssColor(host, token, fallback) {
|
|
313
|
+
const probe = document.createElement("span");
|
|
314
|
+
probe.style.display = "none";
|
|
315
|
+
probe.style.color = `var(${token}, ${fallback})`;
|
|
316
|
+
host.appendChild(probe);
|
|
317
|
+
const resolved = window.getComputedStyle(probe).color;
|
|
318
|
+
probe.remove();
|
|
319
|
+
return resolved || fallback;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
p.setup = () => {
|
|
323
|
+
p.createCanvas(w, h);
|
|
324
|
+
const ink = p.color(cssColor(host, "--accent", "#00e5ff"));
|
|
325
|
+
ink.setAlpha(85); // p5 owns the alpha; the token owns the hue
|
|
326
|
+
p.stroke(ink);
|
|
327
|
+
p.background(p.color(cssColor(host, "--background", "#040a18")));
|
|
328
|
+
};
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Read the tokens **inside `setup`**, not at module scope, so a rebuilt instance
|
|
332
|
+
picks up the current theme. Keep alpha in p5's hands with `setAlpha`: appending
|
|
333
|
+
`"55"` to a token works only if the token happened to be a six-digit hex, and a
|
|
334
|
+
resolved `rgb(...)` string it silently corrupts.
|
|
335
|
+
|
|
336
|
+
**Reading at setup is not enough on its own, because a restyle does not rebuild
|
|
337
|
+
the sketch.** The Style tab recolors a running app by swapping the page's CSS
|
|
338
|
+
variables in place — it replaces the contents of a `<style>` element in the
|
|
339
|
+
document head, with no remount — so every styled element re-renders through the
|
|
340
|
+
cascade and the canvas, which is painted rather than styled, keeps the colours it
|
|
341
|
+
resolved at setup until something unrelated happens to rebuild it. Watch the
|
|
342
|
+
document's stylesheets and repaint:
|
|
343
|
+
|
|
344
|
+
```jsx
|
|
345
|
+
// in the sketch: re-read and repaint on demand
|
|
346
|
+
p.retheme = () => {
|
|
347
|
+
if (palette === null) return; // setup has not run yet
|
|
348
|
+
readPalette();
|
|
349
|
+
p.stroke(palette.ink);
|
|
350
|
+
p.background(palette.bg);
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// in the component: same effect that built the instance, same cleanup
|
|
354
|
+
const instance = new p5(makeSketch({ params, host: node }), node);
|
|
355
|
+
const observer = new MutationObserver(() => instance.retheme());
|
|
356
|
+
observer.observe(document.head, { childList: true, subtree: true, characterData: true });
|
|
357
|
+
return () => {
|
|
358
|
+
observer.disconnect();
|
|
359
|
+
instance.remove();
|
|
360
|
+
};
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
`characterData` and `subtree` are both needed: a first push APPENDS the style
|
|
364
|
+
element, and every push after it rewrites the text of the one already there. The
|
|
365
|
+
observer belongs to the effect that built the instance, so it is disconnected in
|
|
366
|
+
the same cleanup that removes it — an observer outliving its sketch is the same
|
|
367
|
+
leak as a draw loop outliving its canvas.
|
|
368
|
+
|
|
369
|
+
## Determinism — a picture is a handful of numbers
|
|
370
|
+
|
|
371
|
+
**Call `p.randomSeed(seed)` and `p.noiseSeed(seed)` in `setup` so the sketch replays from its numbers instead of from its pixels.**
|
|
372
|
+
Seed both generators and the simulation becomes reproducible: the same seed
|
|
373
|
+
gives the same starting positions, the same wind, the same drift.
|
|
374
|
+
|
|
375
|
+
This is the single most useful p5 idea for a vibe with a database. A saved piece
|
|
376
|
+
is not a PNG — it is its parameters, its seed and **how far the simulation had
|
|
377
|
+
run**, a few dozen bytes, and a fresh instance cooks that recipe again as many
|
|
378
|
+
times as you like. So persist the numbers, not the pixels:
|
|
379
|
+
|
|
380
|
+
`steps` is as load-bearing as `seed` — an animated sketch looks different at step
|
|
381
|
+
100 and step 10,000 — and it is the number that is hardest to get at, because
|
|
382
|
+
**`p` exists only inside the sketch factory and the Keep button lives in React**.
|
|
383
|
+
Reaching for `p.frameCount` from a click handler is a `ReferenceError`. The
|
|
384
|
+
sketch has to publish the count outward; a ref is the smallest way:
|
|
385
|
+
|
|
386
|
+
```jsx
|
|
387
|
+
// in the component
|
|
388
|
+
const stepsRef = React.useRef(0);
|
|
389
|
+
const { database, useLiveQuery } = useFireproof("pieces");
|
|
390
|
+
|
|
391
|
+
React.useEffect(() => {
|
|
392
|
+
const instance = new p5(makeSketch({ params, host: node, stepsRef }), node);
|
|
393
|
+
return () => instance.remove();
|
|
394
|
+
}, [key]);
|
|
395
|
+
|
|
396
|
+
// in the sketch factory — `stepsRef` is an argument, so the closure can write it.
|
|
397
|
+
// ONE counter with ONE owner: `advance()` is the only thing that increments it,
|
|
398
|
+
// because it is the only thing that moves the simulation. Everything else reads.
|
|
399
|
+
let steps = 0;
|
|
400
|
+
const advance = () => {
|
|
401
|
+
steps += 1;
|
|
402
|
+
// …move every particle, draw its segment…
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
p.draw = () => {
|
|
406
|
+
step(); // calls advance() exactly once
|
|
407
|
+
if (stepsRef) stepsRef.current = steps;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// in the Keep handler — no `p` in sight
|
|
411
|
+
async function keep() {
|
|
412
|
+
await database.put({ ...params, type: "piece", steps: stepsRef.current, created: Date.now() });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const { docs } = useLiveQuery("created", { descending: true, limit: 12 });
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Keep the counter to one owner. It counts calls to `advance()`, so incrementing it
|
|
419
|
+
anywhere else — in `draw`, in `step`, beside the publish — counts the same frame
|
|
420
|
+
twice, and a card then replays twice the frames that were on screen when Keep was
|
|
421
|
+
pressed, which is the exact error persisting `steps` exists to prevent.
|
|
422
|
+
|
|
423
|
+
A card then renders exactly `doc.steps` steps (see the still-render pattern
|
|
424
|
+
below), so the wall shows the piece at the moment it was kept rather than at
|
|
425
|
+
whatever frame the card happened to reach. Loading somebody else's piece hands
|
|
426
|
+
the reader a live sketch with their parameters in it, so nudging one number is a
|
|
427
|
+
variation rather than a copy.
|
|
428
|
+
|
|
429
|
+
Two honest limits, and say them in the app's own copy rather than promising more
|
|
430
|
+
than it does:
|
|
431
|
+
|
|
432
|
+
- **Hand-drawn input is not in the numbers.** If a person can drag on the canvas
|
|
433
|
+
to push ink into it, what they drew is unbounded input that no seed replays. A
|
|
434
|
+
kept piece is a canonical render of the recipe, not a snapshot of the canvas at
|
|
435
|
+
the moment of keeping — the recipe is exact, the print is fresh. An app that
|
|
436
|
+
needs the literal pixels is asking for an image, so capture one
|
|
437
|
+
(`p.saveCanvas`, or the canvas's own `toBlob`) and store that instead.
|
|
438
|
+
- **The numbers fix the simulation, not the screen.** A canvas of a different
|
|
439
|
+
size or pixel density starts the particles in proportionally different places,
|
|
440
|
+
the way the same negative prints differently at different sizes.
|
|
441
|
+
|
|
442
|
+
`Math.random()` is NOT covered by `randomSeed` — use `p.random()` everywhere
|
|
443
|
+
inside a sketch that means to be reproducible, and `p.noise()` rather than any
|
|
444
|
+
hand-rolled noise.
|
|
445
|
+
|
|
446
|
+
## Still renders — a thumbnail is a loop you ran to completion
|
|
447
|
+
|
|
448
|
+
To draw a gallery card, run the simulation inside `setup` for the stored number
|
|
449
|
+
of steps and then stop the loop. Same sketch factory, one flag — and `steps`
|
|
450
|
+
comes off the saved piece (`doc.steps`), which is what makes the card the piece
|
|
451
|
+
its author kept:
|
|
452
|
+
|
|
453
|
+
```js
|
|
454
|
+
// ONE frame of the piece, called by both paths. The fade is part of a frame:
|
|
455
|
+
// a still loop that calls only `advance()` accumulates different stroke opacity
|
|
456
|
+
// than the live canvas did, so the card would not be the piece at identical
|
|
457
|
+
// seed, parameters, size and step count.
|
|
458
|
+
const step = () => {
|
|
459
|
+
p.noStroke();
|
|
460
|
+
p.fill(palette.fade);
|
|
461
|
+
p.rect(0, 0, w, h);
|
|
462
|
+
p.stroke(palette.ink);
|
|
463
|
+
advance();
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
p.setup = () => {
|
|
467
|
+
p.createCanvas(w, h);
|
|
468
|
+
p.pixelDensity(still ? 1 : Math.min(window.devicePixelRatio || 1, 2));
|
|
469
|
+
p.randomSeed(params.seed);
|
|
470
|
+
p.noiseSeed(params.seed);
|
|
471
|
+
readPalette();
|
|
472
|
+
p.background(palette.bg);
|
|
473
|
+
spawn();
|
|
474
|
+
if (still) {
|
|
475
|
+
for (let i = 0; i < steps; i++) step(); // the SAME step, not advance()
|
|
476
|
+
p.noLoop();
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
p.draw = () => {
|
|
481
|
+
if (still) return;
|
|
482
|
+
step();
|
|
483
|
+
};
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Whatever a live frame does — the fade, the stroke colour, a per-frame drift — the
|
|
487
|
+
still path has to do too, or the card is a different picture from the one its
|
|
488
|
+
author kept. `p.noLoop()` stops `draw` from being scheduled; `p.loop()` starts it again, and
|
|
489
|
+
`p.redraw()` runs one frame on demand. A wall of stills is cheap because none
|
|
490
|
+
of them are animating.
|
|
491
|
+
|
|
492
|
+
## Performance
|
|
493
|
+
|
|
494
|
+
A flow field draws thousands of segments a frame, so the per-item work is what
|
|
495
|
+
decides whether it holds sixty frames a second.
|
|
496
|
+
|
|
497
|
+
**Group by colour so `stroke()` changes a handful of times per frame instead of
|
|
498
|
+
once per particle.** Bucket particles by palette band, sort by colour, or just
|
|
499
|
+
guard the call:
|
|
500
|
+
|
|
501
|
+
```js
|
|
502
|
+
let cur = null;
|
|
503
|
+
for (const q of particles) {
|
|
504
|
+
if (q.c !== cur) {
|
|
505
|
+
cur = q.c;
|
|
506
|
+
p.stroke(cur);
|
|
507
|
+
}
|
|
508
|
+
p.line(q.px, q.py, q.x, q.y);
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**Rotate coordinates by hand instead of `push`/`translate`/`rotate`/`pop` per
|
|
513
|
+
item.** Precompute the sine and cosine of each symmetry arm once, then transform
|
|
514
|
+
the two endpoints with arithmetic — the difference between one transform matrix
|
|
515
|
+
and eleven thousand of them.
|
|
516
|
+
|
|
517
|
+
```js
|
|
518
|
+
const arms = [];
|
|
519
|
+
for (let s = 0; s < symmetry; s++) {
|
|
520
|
+
const a = (TAU / symmetry) * s;
|
|
521
|
+
arms.push([Math.cos(a), Math.sin(a)]);
|
|
522
|
+
}
|
|
523
|
+
// per segment:
|
|
524
|
+
for (const [co, si] of arms) {
|
|
525
|
+
p.line(cx + ax * co - ay * si, cy + ax * si + ay * co, cx + bx * co - by * si, cy + bx * si + by * co);
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
**Use a translucent rectangle for trails instead of `background()`.** Painting a
|
|
530
|
+
nearly-transparent rect over the canvas each frame dims the history a few
|
|
531
|
+
percent rather than erasing it, which is most of what people mean when a sketch
|
|
532
|
+
"looks generative". The alpha is the knob: lower is smokier, higher snaps back
|
|
533
|
+
to dots.
|
|
534
|
+
|
|
535
|
+
```js
|
|
536
|
+
p.noStroke();
|
|
537
|
+
p.fill(20, 20, 20, 12);
|
|
538
|
+
p.rect(0, 0, width, height);
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Other levers: keep the particle array a fixed size and recycle dead particles
|
|
542
|
+
rather than allocating; keep React state out of the frame loop entirely (the
|
|
543
|
+
loop mutates plain objects the sketch closed over, and React only ever sets the
|
|
544
|
+
parameters); and for a field dense enough that 2D runs out of road, `WEBGL` mode
|
|
545
|
+
with `p.createShader(vert, frag)` evaluates it per pixel on the GPU.
|
|
546
|
+
|
|
547
|
+
## Mouse and touch
|
|
548
|
+
|
|
549
|
+
p5 keeps `p.mouseX` / `p.mouseY` current and fills them from a touch too, so one
|
|
550
|
+
handler usually covers both surfaces.
|
|
551
|
+
|
|
552
|
+
```js
|
|
553
|
+
p.mouseDragged = () => {
|
|
554
|
+
paintAt(p.mouseX, p.mouseY);
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
p.touchMoved = () => {
|
|
558
|
+
paintAt(p.mouseX, p.mouseY);
|
|
559
|
+
return false; // returning false prevents the browser's default scroll/zoom
|
|
560
|
+
};
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
**Set `touchAction: "none"` on the host element** so a drag paints instead of
|
|
564
|
+
scrolling the page on a phone. Returning `false` from `touchMoved` handles the
|
|
565
|
+
gesture p5 sees; the CSS handles the gesture the browser would have taken first.
|
|
566
|
+
|
|
567
|
+
## API surface
|
|
568
|
+
|
|
569
|
+
### Canvas and lifecycle
|
|
570
|
+
|
|
571
|
+
```js
|
|
572
|
+
p.createCanvas(w, h); // or p.createCanvas(w, h, p.WEBGL)
|
|
573
|
+
p.resizeCanvas(w, h);
|
|
574
|
+
p.pixelDensity(1);
|
|
575
|
+
p.noLoop();
|
|
576
|
+
p.loop();
|
|
577
|
+
p.redraw();
|
|
578
|
+
p.frameRate(30);
|
|
579
|
+
p.frameCount; // frames since setup
|
|
580
|
+
p.deltaTime; // ms since the previous frame
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Drawing
|
|
584
|
+
|
|
585
|
+
```js
|
|
586
|
+
p.background("#040a18");
|
|
587
|
+
p.fill(255, 120, 0, 40); // r, g, b, alpha 0-255
|
|
588
|
+
p.noFill();
|
|
589
|
+
p.stroke("#00e5ff55"); // 8-digit hex carries alpha
|
|
590
|
+
p.noStroke();
|
|
591
|
+
p.strokeWeight(1.5);
|
|
592
|
+
|
|
593
|
+
p.line(x1, y1, x2, y2);
|
|
594
|
+
p.circle(x, y, d);
|
|
595
|
+
p.ellipse(x, y, w, h);
|
|
596
|
+
p.rect(x, y, w, h, radius);
|
|
597
|
+
p.triangle(x1, y1, x2, y2, x3, y3);
|
|
598
|
+
p.point(x, y);
|
|
599
|
+
|
|
600
|
+
p.beginShape();
|
|
601
|
+
p.vertex(x, y);
|
|
602
|
+
p.curveVertex(x, y);
|
|
603
|
+
p.endShape(p.CLOSE);
|
|
604
|
+
|
|
605
|
+
p.text("hello", x, y);
|
|
606
|
+
p.textSize(16);
|
|
607
|
+
p.textAlign(p.CENTER, p.CENTER);
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### Colour
|
|
611
|
+
|
|
612
|
+
```js
|
|
613
|
+
p.colorMode(p.HSB, 360, 100, 100, 1);
|
|
614
|
+
const c = p.color(200, 80, 90);
|
|
615
|
+
p.lerpColor(a, b, 0.3);
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### Numbers, randomness, noise
|
|
619
|
+
|
|
620
|
+
```js
|
|
621
|
+
p.random(max); // p.random(min, max), p.random(array)
|
|
622
|
+
p.randomSeed(1234);
|
|
623
|
+
p.noise(x, y, z); // 0..1 Perlin noise
|
|
624
|
+
p.noiseSeed(1234);
|
|
625
|
+
p.noiseDetail(octaves, falloff);
|
|
626
|
+
p.map(v, inMin, inMax, outMin, outMax);
|
|
627
|
+
p.constrain(v, min, max);
|
|
628
|
+
p.lerp(a, b, t);
|
|
629
|
+
p.dist(x1, y1, x2, y2);
|
|
630
|
+
p.TWO_PI;
|
|
631
|
+
p.sin(a);
|
|
632
|
+
p.cos(a);
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
### Transforms
|
|
636
|
+
|
|
637
|
+
```js
|
|
638
|
+
p.push();
|
|
639
|
+
p.translate(x, y);
|
|
640
|
+
p.rotate(angle);
|
|
641
|
+
p.scale(s);
|
|
642
|
+
p.pop();
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Input
|
|
646
|
+
|
|
647
|
+
```js
|
|
648
|
+
p.mouseX;
|
|
649
|
+
p.mouseY;
|
|
650
|
+
p.pmouseX;
|
|
651
|
+
p.pmouseY;
|
|
652
|
+
p.mouseIsPressed;
|
|
653
|
+
p.mousePressed = () => {};
|
|
654
|
+
p.mouseDragged = () => {};
|
|
655
|
+
p.mouseReleased = () => {};
|
|
656
|
+
p.mouseWheel = (event) => {};
|
|
657
|
+
p.touchStarted = () => {};
|
|
658
|
+
p.touchMoved = () => false;
|
|
659
|
+
p.touchEnded = () => {};
|
|
660
|
+
p.keyPressed = () => {};
|
|
661
|
+
p.key;
|
|
662
|
+
p.keyCode;
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Pixels and export
|
|
666
|
+
|
|
667
|
+
```js
|
|
668
|
+
p.loadPixels();
|
|
669
|
+
p.pixels; // flat RGBA array
|
|
670
|
+
p.updatePixels();
|
|
671
|
+
p.get(x, y); // one pixel
|
|
672
|
+
p.createGraphics(w, h); // an offscreen buffer to draw into
|
|
673
|
+
p.saveCanvas("piece", "png");
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### WEBGL
|
|
677
|
+
|
|
678
|
+
```js
|
|
679
|
+
const shader = p.createShader(vertSource, fragSource);
|
|
680
|
+
p.shader(shader);
|
|
681
|
+
shader.setUniform("u_time", p.millis() / 1000);
|
|
682
|
+
shader.setUniform("u_resolution", [w, h]);
|
|
683
|
+
p.rect(-w / 2, -h / 2, w, h); // a full-canvas quad for the fragment shader
|
|
684
|
+
p.box(50);
|
|
685
|
+
p.sphere(40);
|
|
686
|
+
p.orbitControl();
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
## Common gotchas
|
|
690
|
+
|
|
691
|
+
**A global `function setup()` never runs.** In instance mode p5 only calls the
|
|
692
|
+
hooks it finds on the instance, so a sketch written the tutorial way mounts a
|
|
693
|
+
blank canvas and nothing reports a problem.
|
|
694
|
+
|
|
695
|
+
**A missing `instance.remove()` is invisible until it is loud.** The symptom is
|
|
696
|
+
a sketch that speeds up, brightens, or tears after a few parameter changes —
|
|
697
|
+
that is two or four draw loops painting the same canvas.
|
|
698
|
+
|
|
699
|
+
**`p.resizeCanvas` clears the drawing.** Repaint the background and reseed the
|
|
700
|
+
simulation from `windowResized` rather than assuming the previous frame survived.
|
|
701
|
+
|
|
702
|
+
**Do not drive the sketch from React state per frame.** A `setState` inside
|
|
703
|
+
`draw` re-renders the tree sixty times a second; let the loop mutate plain
|
|
704
|
+
objects and keep React for the controls.
|
|
705
|
+
|
|
706
|
+
**`p.random()` is seeded; `Math.random()` is not.** Mixing them makes a sketch
|
|
707
|
+
that looks reproducible until it isn't.
|
|
708
|
+
|
|
709
|
+
**Colour with alpha is how trails and depth are made.** An 8-digit hex string
|
|
710
|
+
(`"#00e5ff55"`) or a fourth argument to `fill`/`stroke` is usually the
|
|
711
|
+
difference between a diagram and a picture.
|
|
712
|
+
|
|
713
|
+
**One instance per host element.** Mounting a second p5 into the same node gives
|
|
714
|
+
two canvases stacked in the same box.
|