@vibes.diy/prompts 14.1.20 → 14.1.21

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 CHANGED
@@ -368,7 +368,7 @@ export function habits(doc, oldDoc, user, ctx) {
368
368
 
369
369
  The leaderboard is just the access model: read the public `track:` channels and sum them; each viewer additionally sees their own streaks and any buddy who granted them in. A **plain daily habit tracker** — one framed as the user's own, with no catalog, leaderboard, or buddy asked for — is instead the per-visitor shape shown above: every visitor's habits and check-ins on their single `user:${user.userHandle}` channel, self-granted and private to them.
370
370
 
371
- **"Invite", "join", "people can join", "collaborate", "share with", "together", "with my partner/team", or a board/canvas/room/whiteboard a group co-edits → per-object collaboration** (the second worked example above) — each shared thing is ONE object its members reach directly; it needs no owner. Use the per-object recipe: a channel per object (`board:<id>`/`list:<id>`); the creator self-grants at creation (`grant: { users: { [user.userHandle]: [ch] } }`); child docs gate on `ctx.requireAccess(ch)` so any member edits any child in it (not just their own); a member-authored `share` doc grants a peer the same channel; a `request` doc — which takes **no** `requireAccess` — lets a not-yet-member ask to join. Keep the _object's own_ creator field write-once (`if (oldDoc && doc.author !== oldDoc.author) throw`) and a child's object-id immutable. **Two traps to avoid:** don't build it as an open public feed where each person only owns their own items (that abandons the shared membership), and don't gate it behind a single writer (members self-serve via share/request).
371
+ **"Invite", "join", "people can join", "collaborate", "share with", "together", "with my partner/team", or a board/canvas/room/whiteboard a group co-edits → per-object collaboration** (the second worked example above) — each shared thing is ONE object its members reach directly; it needs no owner. Use the per-object recipe: a channel per object (`board:<id>`/`list:<id>`); the creator self-grants at creation (`grant: { users: { [user.userHandle]: [ch] } }`); child docs gate on `ctx.requireAccess(ch)` so any member edits any child in it (not just their own); a member-authored `share` doc grants a peer the same channel; a `request` doc — which takes **no** `requireAccess` — lets a not-yet-member ask to join. Keep the _object's own_ creator field write-once (`if (oldDoc && doc.author !== oldDoc.author) throw`) and a child's object-id immutable. **Two traps to avoid:** don't build it as an open public feed where each person only owns their own items (that abandons the shared membership), and don't gate it behind a single writer (members self-serve via share/request). The app owner is not special in the data model: any signed-in person creates their own object and invites a friend into it, and the rule keys on `user.userHandle` or the object's creator, never on `user.isOwner`.
372
372
 
373
373
  **Ownership is just the object graph** — whoever authored or created a doc owns it (`doc.authorHandle === user.userHandle`, checked against `oldDoc` on updates). There's no broadcaster shape to reach for by default, and **owner-only publishing is a dead end** — never gate the content itself on `requireRole("owner")`. **A blog, magazine, or publication is public read + author-owned posts, with the owner controlling the _author roster_:** the owner approves authors with a grant doc (`if (doc.type === "author") { ctx.requireRole("owner"); return { channels: ["blog:authors"], grant: { users: { [doc.authorHandle]: ["blog:authors"] }, roles: { owner: ["blog:authors"] } } }; }` — the **one** place `requireRole("owner")` belongs, gating who may author, never the posts); a post then gates on `ctx.requireAccess("blog:authors")` (membership) and is author-owned (`doc.authorHandle === user.userHandle` + the `oldDoc` check), so once approved each author's post is _their own_ object — only they edit it, and they moderate the comments on it (a comment is allowed if it's your comment **or** you own the post: `doc.authorHandle === user.userHandle || doc.postAuthorHandle === user.userHandle`). A personal blog is just this with a roster of one. Always gate write UI on `useVibe(dbName).can`.
374
374
 
@@ -674,7 +674,7 @@ Channel `_id` is the channel identifier everywhere. The access function uses `do
674
674
 
675
675
  ### Example: Per-object sharing (collaborate on your own objects, no admin)
676
676
 
677
- Reach for this whenever the prompt says **invite, join, collaborate, share with, together, with my partner/team** — a shared shopping list you invite a partner to, a whiteboard people can join, a trip a group plans together. A list app where every signed-in user makes their own lists, sees only their own, and can invite anyone to collaborate on a specific list — peer to peer, with no app admin in the loop. The pattern: **a channel per object** (`list:<id>`); the creator grants themselves that channel at creation; child docs (items) gate on `ctx.requireAccess` of the list's channel, so **any member edits any item**; any current member shares the list by granting another user the same channel. Membership is direct `grant.users`, so each viewer's access scales with their own memberships.
677
+ Reach for this whenever the prompt says **invite, join, collaborate, share with, together, with my partner/team** — a shared shopping list you invite a partner to, a whiteboard people can join, a trip a group plans together. A list app where every signed-in user makes their own lists, sees only their own, and can invite anyone to collaborate on a specific list — peer to peer, with no app admin in the loop. The pattern: **a channel per object** (`list:<id>`); the creator grants themselves that channel at creation; child docs (items) gate on `ctx.requireAccess` of the list's channel, so **any member edits any item**; any current member shares the list by granting another user the same channel. Membership is direct `grant.users`, so each viewer's access scales with their own memberships. Nothing a person writes reaches anyone else until they chose it: a per-object channel starts with its creator as the only reader, and every other reader arrives through a grant that creator wrote.
678
678
 
679
679
  access.js
680
680
 
@@ -755,6 +755,83 @@ App.jsx — `access.hasChannel()` shows only the lists the viewer belongs to; `u
755
755
  >>>>>>> REPLACE
756
756
  ```
757
757
 
758
+ #### The same shape with a default object per person, and a member doc that names the friend
759
+
760
+ Reach for this variant whenever the app is one shared *thing* people keep coming back to — a board, a trip, a game night's scoreboard, a hiking group's trail list. Everything above still holds; three additions make it feel finished from the first load:
761
+
762
+ - **Every signed-in person already has one object, with no doc to create.** Their default channel is derived from their own handle — `` `board:default-${user.userHandle}` `` — so a brand-new visitor opens the app, types, and their first item lands. Each write to it re-grants that person their own channel, because there is no object doc carrying the grant. **The child doc stores that board id rather than having each writer derive one**: the create writes `` boardId: pickedBoardId || `default-${me.userHandle}` `` and the rule holds it fixed from then on, so a card stays on the board its creator put it on however many people edit it later.
763
+ - **Membership is a `member` doc, and picking the friend is `HandleInput`** (`useViewer()`, see use-viewer.md — the platform's people-picker, so the handle on the doc is one the platform resolved). The doc grants the named person the object's channel: `` grant: { users: { [doc.userHandle]: [chan] } } ``.
764
+ - **The object's creator is its admin**, held on a second channel (`` `${chan}/admin` ``) that the creator self-grants at creation. Adding a member is gated on that admin channel, so members collaborate and the creator decides who joins. The app owner holds no place in any of this: every rule reads `user.userHandle` or the object's own `creatorHandle`.
765
+
766
+ access.js
767
+
768
+ ```js
769
+ export function boards(doc, oldDoc, user, ctx) {
770
+ if (!user?.userHandle) throw { forbidden: "sign in" };
771
+ const safeId = (id) => {
772
+ if (typeof id !== "string" || !/^[A-Za-z0-9_-]+$/.test(id)) throw { forbidden: "bad id" };
773
+ return id;
774
+ };
775
+ const myDefault = "default-" + user.userHandle;
776
+ const ch = (id) => "board:" + safeId(id);
777
+
778
+ if (doc.type === "board") {
779
+ // Anyone signed in makes their own board; the creator holds it and its admin channel.
780
+ if (oldDoc) {
781
+ if (doc.creatorHandle !== oldDoc.creatorHandle) throw { forbidden: "creator is fixed" };
782
+ ctx.requireAccess(ch(doc._id) + "/admin"); // renaming is an admin act — updates only
783
+ } else if (doc.creatorHandle !== user.userHandle) {
784
+ throw { forbidden: "you must be the creator" };
785
+ }
786
+ return { channels: [ch(doc._id)], grant: { users: { [doc.creatorHandle]: [ch(doc._id), ch(doc._id) + "/admin"] } } };
787
+ }
788
+
789
+ if (doc.type === "card") {
790
+ // The card STORES its board id from the moment it is created (the client writes the
791
+ // creator's own default when the person picked no board) and holds it fixed after —
792
+ // including the absent-to-present move, so a later editor's handle always finds the
793
+ // id already on the doc rather than supplying one.
794
+ if (!oldDoc && (typeof doc.boardId !== "string" || doc.boardId.length === 0)) {
795
+ throw { forbidden: "a card names its board" };
796
+ }
797
+ if (oldDoc && doc.boardId !== oldDoc.boardId) throw { forbidden: "card stays on its board" };
798
+ const chan = ch(doc.boardId);
799
+ if (doc.boardId !== myDefault) ctx.requireAccess(chan); // any member edits any card on it
800
+ // The implicit default board has no board doc, so each write re-grants its own person.
801
+ if (doc.boardId === myDefault) {
802
+ return { channels: [chan], grant: { users: { [user.userHandle]: [chan, chan + "/admin"] } } };
803
+ }
804
+ return { channels: [chan] };
805
+ }
806
+
807
+ if (doc.type === "member") {
808
+ // The board's admin invites a person the picker resolved; the doc IS the grant.
809
+ if (oldDoc) throw { forbidden: "membership grants are fixed" };
810
+ if (doc.addedBy !== user.userHandle) throw { forbidden: "addedBy must be you" };
811
+ if (doc.boardId !== myDefault) ctx.requireAccess(ch(doc.boardId) + "/admin");
812
+ return { channels: [ch(doc.boardId)], grant: { users: { [doc.userHandle]: [ch(doc.boardId)] } } };
813
+ }
814
+
815
+ throw { forbidden: "unknown document type" };
816
+ }
817
+ ```
818
+
819
+ App.jsx — the invite surface is `HandleInput` plus one `member` write, shown to whoever the access fn would accept:
820
+
821
+ ```jsx
822
+ const { HandleInput } = useViewer();
823
+ const { me, can } = useVibe("boards");
824
+ const canInvite = (boardId) => can.create({ type: "member", boardId, userHandle: "x", addedBy: me?.userHandle }).ok;
825
+ const addMember = (boardId, handle) =>
826
+ handle && database.put({ type: "member", boardId, userHandle: handle, addedBy: me.userHandle });
827
+ // Every card names its board at creation — the person's own default board when they picked none.
828
+ const addCard = (text) => database.put({ type: "card", boardId: pickedBoardId || `default-${me.userHandle}`, text });
829
+ // …
830
+ {canInvite(board._id) && <HandleInput onChange={(h) => addMember(board._id, h)} placeholder="Add a friend…" />}
831
+ ```
832
+
833
+ Where the app's own maintainer needs to move records through the gate — a CLI migration re-homing cards — `!user.isOwner` reads as a bypass written beside a check (`if (!user.isOwner) ctx.requireAccess(chan)`), never as the arm that decides who may act.
834
+
758
835
  ## More worked round-trip examples
759
836
 
760
837
  ### Example: Workspace chat with channels
package/llms/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { imageGenConfig } from "./image-gen.js";
6
6
  export { webAudioConfig } from "./web-audio.js";
7
7
  export { d3Config } from "./d3.js";
8
8
  export { threeJsConfig } from "./three-js.js";
9
+ export { p5Config } from "./p5.js";
9
10
  export { voxelConfig } from "./voxel.js";
10
11
  export { webxrConfig } from "./webxr.js";
11
12
  export { useViewerConfig } from "./use-viewer.js";
@@ -15,4 +16,4 @@ export { accessConfig } from "./access.js";
15
16
  export { connectionsConfig } from "./connections.js";
16
17
  export { youtubeConfig } from "./youtube.js";
17
18
  export type { LlmConfig } from "./types.js";
18
- export declare const allConfigs: readonly [import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig];
19
+ export declare const allConfigs: readonly [import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig];
package/llms/index.js CHANGED
@@ -6,6 +6,7 @@ import { imageGenConfig } from "./image-gen.js";
6
6
  import { webAudioConfig } from "./web-audio.js";
7
7
  import { d3Config } from "./d3.js";
8
8
  import { threeJsConfig } from "./three-js.js";
9
+ import { p5Config } from "./p5.js";
9
10
  import { voxelConfig } from "./voxel.js";
10
11
  import { webxrConfig } from "./webxr.js";
11
12
  import { useViewerConfig } from "./use-viewer.js";
@@ -22,6 +23,7 @@ export { imageGenConfig } from "./image-gen.js";
22
23
  export { webAudioConfig } from "./web-audio.js";
23
24
  export { d3Config } from "./d3.js";
24
25
  export { threeJsConfig } from "./three-js.js";
26
+ export { p5Config } from "./p5.js";
25
27
  export { voxelConfig } from "./voxel.js";
26
28
  export { webxrConfig } from "./webxr.js";
27
29
  export { useViewerConfig } from "./use-viewer.js";
@@ -36,6 +38,7 @@ export const allConfigs = [
36
38
  webAudioConfig,
37
39
  d3Config,
38
40
  threeJsConfig,
41
+ p5Config,
39
42
  voxelConfig,
40
43
  fireproofConfig,
41
44
  webxrConfig,
package/llms/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../jsr/llms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAI7C,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,YAAY;IACZ,cAAc;IACd,cAAc;IACd,QAAQ;IACR,aAAa;IACb,WAAW;IACX,eAAe;IACf,WAAW;IACX,eAAe;IACf,aAAa;IACb,gBAAgB;IAChB,aAAa;IACb,cAAc;IACd,YAAY;IACZ,iBAAiB;IACjB,aAAa;CACL,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../jsr/llms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAI7C,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,YAAY;IACZ,cAAc;IACd,cAAc;IACd,QAAQ;IACR,aAAa;IACb,QAAQ;IACR,WAAW;IACX,eAAe;IACf,WAAW;IACX,eAAe;IACf,aAAa;IACb,gBAAgB;IAChB,aAAa;IACb,cAAc;IACd,YAAY;IACZ,iBAAiB;IACjB,aAAa;CACL,CAAC"}
package/llms/p5.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { LlmConfig } from "./types.js";
2
+ export declare const p5Config: LlmConfig;
package/llms/p5.js ADDED
@@ -0,0 +1,28 @@
1
+ export const p5Config = {
2
+ name: "p5",
3
+ label: "p5.js",
4
+ description: "p5.js creative-coding library for 2D canvas and WEBGL sketches: generative art, flow " +
5
+ "fields, particle systems, Perlin noise, seeded deterministic sketches, kaleidoscopes, " +
6
+ "screensavers, visualisers, drawing and painting toys, plotter-style patterns, still " +
7
+ "gallery renders with noLoop, and fragment shaders. Covers instance mode in React (one " +
8
+ "useEffect, instance.remove() on cleanup), the version-pinned esm.sh import, sizing from " +
9
+ "the host element, pixelDensity, randomSeed/noiseSeed determinism, trails, and touch. " +
10
+ "p5, p5.js, processing, sketch, creative coding, generative art, canvas art, noise field",
11
+ cues: [
12
+ "p5",
13
+ "p5.js",
14
+ "processing",
15
+ "sketch",
16
+ "generative art",
17
+ "creative coding",
18
+ "flow field",
19
+ "perlin noise",
20
+ "particle field",
21
+ "kaleidoscope",
22
+ "screensaver",
23
+ ],
24
+ importModule: "https://esm.sh/p5@1.11.13",
25
+ importName: "p5",
26
+ importType: "default",
27
+ };
28
+ //# sourceMappingURL=p5.js.map
package/llms/p5.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"p5.js","sourceRoot":"","sources":["../../jsr/llms/p5.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,QAAQ,GAAc;IACjC,IAAI,EAAE,IAAI;IACV,KAAK,EAAE,OAAO;IACd,WAAW,EACT,uFAAuF;QACvF,wFAAwF;QACxF,sFAAsF;QACtF,wFAAwF;QACxF,0FAA0F;QAC1F,uFAAuF;QACvF,yFAAyF;IAC3F,IAAI,EAAE;QACJ,IAAI;QACJ,OAAO;QACP,YAAY;QACZ,QAAQ;QACR,gBAAgB;QAChB,iBAAiB;QACjB,YAAY;QACZ,cAAc;QACd,gBAAgB;QAChB,cAAc;QACd,aAAa;KACd;IAID,YAAY,EAAE,2BAA2B;IACzC,UAAU,EAAE,IAAI;IAChB,UAAU,EAAE,SAAS;CACtB,CAAC"}
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.
@@ -319,3 +319,23 @@ async function assign(handle) {
319
319
  }
320
320
  <HandleInput onChange={assign} placeholder="Add a teammate…" />;
321
321
  ```
322
+
323
+ ### Adding a member to your own object — the canonical use
324
+
325
+ Whenever the app is for a group — a shared list, a board, a trip, a club's page — each signed-in person has their own object and lets a friend into it by picking them here. The write is one small membership doc, and `access.js` turns that doc into the grant that gives the named person the object's channel (see access.md's per-object sharing examples). Keys on the picked handle and on the writer's own — the app owner has no special place in it.
326
+
327
+ ```jsx
328
+ const { HandleInput } = useViewer();
329
+ const { me, can } = useVibe("boards");
330
+ // Show the picker to whoever the access fn would accept as an inviter.
331
+ const canInvite = can.create({ type: "member", boardId: board._id, userHandle: "x", addedBy: me?.userHandle }).ok;
332
+
333
+ async function addMember(handle) {
334
+ if (!handle) return;
335
+ await database.put({ type: "member", boardId: board._id, userHandle: handle, addedBy: me.userHandle });
336
+ }
337
+
338
+ {canInvite && <HandleInput onChange={addMember} placeholder="Add a friend…" />}
339
+ ```
340
+
341
+ Members render with `<ViewerTag userHandle={m.userHandle} />`, the same way any other stored handle does. An app one person keeps for themselves needs none of this — the picker belongs where the prompt asks for other people.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "14.1.20",
3
+ "version": "14.1.21",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "exports": {
@@ -34,9 +34,9 @@
34
34
  "license": "Apache-2.0",
35
35
  "dependencies": {
36
36
  "@adviser/cement": "~0.5.34",
37
- "@vibes.diy/call-ai-v2": "14.1.20",
38
- "@vibes.diy/identity": "14.1.20",
39
- "@vibes.diy/use-vibes-types": "14.1.20",
37
+ "@vibes.diy/call-ai-v2": "14.1.21",
38
+ "@vibes.diy/identity": "14.1.21",
39
+ "@vibes.diy/use-vibes-types": "14.1.21",
40
40
  "arktype": "~2.2.3",
41
41
  "json-schema-faker": "~0.6.3"
42
42
  },
package/system-prompt.md CHANGED
@@ -525,6 +525,8 @@ docs in your database, and never build follow UI state machines.
525
525
 
526
526
  **A single-user tool needs no membership gate — don't invent collaboration the prompt didn't ask for.** Many apps are one person making their own things: an image generator, a "describe X → make X" tool, a solo tracker/composer/calculator. Its user must be able to create from first load with no one letting them in. The failure to avoid: wrapping the primary write in `ctx.requireAccess(channel)` (or a roster / `requireRole` gate) so the signed-in maker hits "not in channel / request to join" on the very app they just made, or a members-only wall that hides the compose input until they "join." Membership machinery — `ctx.requireAccess`, roster approval, `request`/`share` docs, a "request to join" CTA — belongs ONLY where the prompt explicitly asks people to collaborate on, join, or share a space. For a solo tool the right shape is author-owned writes: stamp `authorHandle: user.userHandle` on each doc and let the author check accept it (self-granting each object its own channel is fine — requiring _pre-existing_ membership to write is not), or `allowAnonymous: true` in `access.js` for a no-sign-in tool — gate the write surface the usual way (`ready` for pending identity/access resolution, then `can.create(candidate).ok` for the affordance itself), which works for anonymous visitors too: once `allowAnonymous` opts them in, the access fn accepts `user === null` and `can.create()` returns the real verdict rather than `authentication required`. The footgun is gating the affordance on `can.create()` _without_ setting `allowAnonymous` — then it stays `authentication required` and the control vanishes for the very guests it's for.
527
527
 
528
+ **A group-shaped ask — more than one person acting on the same records: several people adding, voting, sharing or joining, or a named group such as roommates, a book club or a team keeping records together — gives each signed-in person their own object plus a way to invite others into it with the platform's `HandleInput` (from `useViewer()`), and keys its rules on that object's own creator (`doc.creatorHandle === user.userHandle`), which places the app owner exactly where everyone else stands.**
529
+
528
530
  **The owner must never be locked out of their own app.** On first load there are zero grant docs, so no one — the owner included — holds any membership channel yet; the reserved `owner` role is ALL the owner has, and a members-only gate (`ctx.requireAccess(ch)`) denies the owner exactly like a stranger. Two rules keep the owner in. In `access.js`: when a grant branch is itself owner-gated (`ctx.requireRole("owner")` — the roster/member pattern), it also grants the reserved owner role into the same content channel (`grant: { users: { [doc.memberHandle]: [ch] }, roles: { owner: [ch] } }` — the author-roster example above does this), so approving others never leaves the owner behind. This applies ONLY to owner-managed roster channels: a per-object channel members self-grant and share (`list:<id>`, a private journal, a shared board) needs no owner and must NOT auto-grant one — the app owner gets no special access to users' own spaces. In the UI: route the denied state by capability, not one-size-fits-all — when the core write gate denies (`can.create({ type: "post", ... }).ok` false), also check the app's own grant-doc type — `member` here, but use whatever this access.js names it (`author`, `share`, `approve`): `can.create({ type: "member", userHandle: me?.userHandle }).ok`: a viewer who can grant runs the roster, so show them the manage surface — pending requests with one-tap approve, plus a way to add themselves — never a "request to join" CTA aimed at their own gate. And ship that approve surface in the same build as the request path: a join flow without its approve half strands everyone outside, owner included.
529
531
 
530
532
  **Author-equality gates `create` and ownership change — NOT every update.** A shared-visible doc (public read, a gallery/catalog others browse) that `<ImgGen>` appends onto is written by whoever is _looking at it_: a version append runs as the VIEWING user, so a blanket `if (oldDoc && doc.authorHandle !== user.userHandle) throw` denies every other viewer's generation — after it was already billed — arming an unbounded billed-retry loop (#3784/#3832). Fix the author at create, forbid re-authoring, and for a non-author update accept only a legitimate ImgGen version append — the platform predicate `ctx.isImgGenVersionAppend(doc, oldDoc)` decides that (`oldDoc` is `null` on create):