@hraness/slopcamera 3.6.0 → 3.8.0

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.
Files changed (57) hide show
  1. package/README.md +49 -33
  2. package/apps/desktop/README.md +3 -10
  3. package/apps/desktop/application/html-overlay-renderer.ts +6 -0
  4. package/apps/desktop/application/operation.ts +1 -1
  5. package/apps/desktop/application/spatial-assets.ts +1 -1
  6. package/apps/desktop/cli/{menubar-status.ts → activity-status.ts} +24 -21
  7. package/apps/desktop/cli/args.ts +62 -10
  8. package/apps/desktop/cli/command-host-resources.ts +5 -1
  9. package/apps/desktop/cli/commands.ts +26 -9
  10. package/apps/desktop/cli/desktop-status.ts +271 -0
  11. package/apps/desktop/cli/help.ts +36 -28
  12. package/apps/desktop/cli/html-film-init.ts +158 -0
  13. package/apps/desktop/cli/html-film-names.ts +17 -0
  14. package/apps/desktop/cli/html-film.ts +471 -0
  15. package/apps/desktop/cli/html-overlay-renderer.ts +29 -2
  16. package/apps/desktop/cli/html-scene.ts +3 -3
  17. package/apps/desktop/cli/legacy-login.ts +184 -0
  18. package/apps/desktop/cli/main.ts +9 -0
  19. package/apps/desktop/cli/outputs.ts +19 -0
  20. package/apps/desktop/cli/portable-surface.ts +55 -1
  21. package/apps/desktop/cli/registry.ts +314 -0
  22. package/apps/desktop/cli/verbs.ts +193 -0
  23. package/apps/desktop/dist/cli/main.js +338 -316
  24. package/apps/desktop/html-overlay/film.ts +418 -0
  25. package/dist/cli.js +13 -2
  26. package/dist/generate.js +1 -1
  27. package/dist/{index-4z9y2wa2.js → index-842t02pp.js} +1 -1
  28. package/dist/{index-qxfexvj3.js → index-k9bjw80z.js} +20 -4
  29. package/dist/index-p74xy76d.js +3 -0
  30. package/dist/{index-fevf4ppk.js → index-y8spxs77.js} +10 -7
  31. package/dist/index.js +1 -1
  32. package/dist/operations.js +1 -1
  33. package/dist/workflow.js +1 -1
  34. package/docs/README.md +6 -6
  35. package/examples/html/launch-film/README.md +51 -0
  36. package/examples/html/launch-film/build.ts +158 -0
  37. package/examples/html/launch-film/film.css +593 -0
  38. package/examples/html/launch-film/film.html +85 -0
  39. package/examples/html/launch-film/film.js +410 -0
  40. package/examples/html/launch-film/film.json +63 -0
  41. package/examples/html/launch-film/mockups.tsx +74 -0
  42. package/examples/html/launch-film/timeline.ts +57 -0
  43. package/package.json +24 -11
  44. package/skills/slopcamera/SKILL.md +4 -3
  45. package/skills/slopcamera/references/install.md +3 -3
  46. package/skills/slopcamera/references/launch-films.md +158 -0
  47. package/skills/slopcamera/references/reference-led-3d.md +1 -1
  48. package/skills/slopcamera/references/visual-communication.md +1 -1
  49. package/src/cli.ts +125 -9
  50. package/src/generation-pricing.ts +21 -1
  51. package/src/icon-set.ts +1086 -0
  52. package/src/icon.ts +396 -59
  53. package/src/index.ts +1 -0
  54. package/src/operations.ts +52 -0
  55. package/src/version.ts +1 -1
  56. package/apps/desktop/cli/menubar.ts +0 -272
  57. package/dist/index-nxtmy18t.js +0 -3
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hraness/slopcamera",
3
- "version": "3.6.0",
4
- "description": "Slopcamera lets your coding agent make images, diagrams, animation, 3D scenes, and video from source files it can keep revising.",
3
+ "version": "3.8.0",
4
+ "description": "SlopCamera lets your coding agent make images, diagrams, animation, 3D scenes, and video from source files it can keep revising.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "sideEffects": [
@@ -27,8 +27,8 @@
27
27
  ],
28
28
  "catalog": {
29
29
  "@ai-sdk/gateway-v4": "npm:@ai-sdk/gateway@^4.0.27",
30
- "@img/sharp-darwin-arm64": "0.35.3",
31
- "@img/sharp-libvips-darwin-arm64": "1.3.2",
30
+ "@img/sharp-darwin-arm64": "0.35.4",
31
+ "@img/sharp-libvips-darwin-arm64": "1.3.3",
32
32
  "@native-sdk/cli": "0.5.3",
33
33
  "@types/bun": "^1.3.14",
34
34
  "@types/node": "^24.10.0",
@@ -131,6 +131,10 @@
131
131
  "./local/html-overlay": {
132
132
  "types": "./apps/desktop/html-overlay/index.ts",
133
133
  "import": "./apps/desktop/html-overlay/index.ts"
134
+ },
135
+ "./local/html-film": {
136
+ "types": "./apps/desktop/html-overlay/film.ts",
137
+ "import": "./apps/desktop/html-overlay/film.ts"
134
138
  }
135
139
  },
136
140
  "bin": {
@@ -161,6 +165,8 @@
161
165
  "examples/studio/hybrid-scene.ts",
162
166
  "examples/html/*.html",
163
167
  "examples/html/*.json",
168
+ "examples/html/launch-film/*",
169
+ "!examples/html/launch-film/out",
164
170
  "examples/style-portfolio/**/*.html",
165
171
  "examples/style-portfolio/**/*.ts",
166
172
  "examples/style-portfolio/**/*.mjs",
@@ -219,6 +225,9 @@
219
225
  "test": "bun run test:sdk && bun run test:desktop && bun run test:api && bun run --cwd apps/web test",
220
226
  "test:sdk": "bun test ./src ./packages/scene",
221
227
  "test:desktop": "bun test ./apps/desktop/contracts ./apps/desktop/core ./apps/desktop/application ./apps/desktop/code ./apps/desktop/workflows ./apps/desktop/html-overlay ./apps/desktop/cli ./apps/desktop/analysis ./apps/desktop/studio",
228
+ "test:desktop:code": "bun test ./apps/desktop/code",
229
+ "test:desktop:cli": "bun test ./apps/desktop/cli",
230
+ "test:desktop:rest": "bun test ./apps/desktop/contracts ./apps/desktop/core ./apps/desktop/application ./apps/desktop/workflows ./apps/desktop/html-overlay ./apps/desktop/analysis ./apps/desktop/studio",
222
231
  "build:desktop:analysis:macos": "bun run ./apps/desktop/analysis/build.ts",
223
232
  "build:cli:macos": "bun run ./apps/desktop/cli/build-compiled.ts",
224
233
  "test:cli:compiled:macos": "bun run build:cli:macos && SLOPCAMERA_RUN_COMPILED_CLI_SMOKE=1 bun test ./apps/desktop/cli/compiled-portable-surface.macos.test.ts",
@@ -243,10 +252,13 @@
243
252
  "check:sdk": "bun run typecheck:sdk && bun run lint:sdk && bun run build:sdk && bun run test:sdk && bun run check:release-workflows && bun run check:schema && bun run check:skill",
244
253
  "check:desktop": "bun run check:effect && bun run typecheck:desktop && bun run lint:desktop && bun run test:desktop && bun run build:desktop",
245
254
  "check:web": "bun apps/web/scripts/verify-example-sources.ts && bun run --cwd apps/web check",
246
- "kb:refresh": "bunx --bun github:hraness/kb#v0.15.2 refresh --root kb",
247
- "kb:check:lane": "bunx --bun github:hraness/kb#v0.15.2 check --root kb --no-catalog",
248
- "kb:check": "bunx --bun github:hraness/kb#v0.15.2 check --root kb && bunx --bun github:hraness/kb#v0.15.2 agents check --root kb --repo .",
249
- "kb:catalog": "bunx --bun github:hraness/kb#v0.15.2 catalog --root kb",
255
+ "check:web:compile": "bun run --cwd apps/web test:compile",
256
+ "check:web:verify": "bun apps/web/scripts/verify-example-sources.ts && bun run --cwd apps/web check:verify",
257
+ "kb:refresh": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.24.0/hraness-wordcell-0.24.0.tgz wordcell refresh --root kb",
258
+ "kb:check:lane": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.24.0/hraness-wordcell-0.24.0.tgz wordcell check --root kb --no-catalog",
259
+ "kb:check": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.24.0/hraness-wordcell-0.24.0.tgz wordcell check --root kb && bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.24.0/hraness-wordcell-0.24.0.tgz wordcell agents check --root kb --repo .",
260
+ "kb:catalog": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.24.0/hraness-wordcell-0.24.0.tgz wordcell catalog --root kb",
261
+ "kb:search": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.24.0/hraness-wordcell-0.24.0.tgz wordcell search --root kb --mode exact --selected-passage",
250
262
  "check:cost-surfaces": "bun ./scripts/check-cost-surfaces.mjs",
251
263
  "check": "bun run check:cost-surfaces && bun run check:standalone && bun run check:copy && bun run check:sdk && bun run check:desktop && bun run check:api && bun run check:web && bun run check:standalone && bun run test:package",
252
264
  "prepack": "bun run check",
@@ -260,6 +272,7 @@
260
272
  "dependencies": {
261
273
  "@ai-sdk/gateway-v4": "npm:@ai-sdk/gateway@^4.0.27",
262
274
  "@hraness/algal": "github:hraness/algal#c46264a",
275
+ "@hraness/desktop-foundation": "https://github.com/hraness/desktop-foundation/releases/download/v0.9.0/hraness-desktop-foundation-0.9.0.tgz",
263
276
  "@hraness/support-foundation": "github:hraness/support-foundation#8bb514d24b79dc3f305390700ae312cab88e7ad2",
264
277
  "@noble/hashes": "2.2.0",
265
278
  "@paper-design/shaders": "0.0.77",
@@ -272,14 +285,14 @@
272
285
  "playwright-core": "1.62.0",
273
286
  "react": "19.2.3",
274
287
  "react-dom": "19.2.3",
275
- "sharp": "0.35.3",
288
+ "sharp": "0.35.4",
276
289
  "three": "0.183.2",
277
290
  "typescript": "^6.0.3",
278
291
  "zod": "^4.4.3"
279
292
  },
280
293
  "optionalDependencies": {
281
- "@img/sharp-darwin-arm64": "0.35.3",
282
- "@img/sharp-libvips-darwin-arm64": "1.3.2"
294
+ "@img/sharp-darwin-arm64": "0.35.4",
295
+ "@img/sharp-libvips-darwin-arm64": "1.3.3"
283
296
  },
284
297
  "devDependencies": {
285
298
  "@antithesishq/bombadil": "0.7.2",
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: slopcamera
3
- description: Create, inspect, edit and render visual media with the Slopcamera CLI and SDK. Use for diagrams, raster-to-SVG conversion, existing screen or camera recordings, video editing and captions, social collage banners, music videos from authored scenes and local tracks, AI images/video/speech/transcription, Three.js scenes and world-space media, Blender rigs and simulation, CadQuery models, parametric architectural designs, Manim educational films, HTML/WGSL motion graphics, and durable agent workflows.
3
+ description: Create, inspect, edit and render visual media with the SlopCamera CLI and SDK. Use for diagrams, raster-to-SVG conversion, existing screen or camera recordings, video editing and captions, social collage banners, music videos from authored scenes and local tracks, AI images/video/speech/transcription, Three.js scenes and world-space media, Blender rigs and simulation, CadQuery models, parametric architectural designs, Manim educational films, HTML/WGSL motion graphics, and durable agent workflows.
4
4
  ---
5
5
 
6
- # Create visual media with Slopcamera
6
+ # Create visual media with SlopCamera
7
7
 
8
8
  Start from the requested result and existing source. Use the installed `slopcamera` CLI, inspect its real capabilities and retain the authored source beside its derived media. Read only the references needed by the task.
9
9
 
@@ -17,7 +17,7 @@ materials, movement, and finishing together.
17
17
 
18
18
  ## Establish the host
19
19
 
20
- Run `slopcamera --help` and use `slopcamera doctor --json` for the selected local tools. If installation or a version mismatch blocks the task, read [installation and readiness](references/install.md). A skill installation does not install the CLI or native engines.
20
+ Run `slopcamera --help` and use `slopcamera doctor --json` for the selected local tools. `slopcamera status --json` reports what is running, credits and the newest outputs; `slopcamera commands --json` lists every command with what it may change. If installation or a version mismatch blocks the task, read [installation and readiness](references/install.md). A skill installation does not install the CLI or native engines.
21
21
 
22
22
  Slopcamera installs from its verified release archive or from source. Historical Atet archives contain the old package and executable, not these renamed commands. Discover IDs and options through help, `operations`, `workflows`, `ai models` and `html catalog`; never invent them or silently switch to historical paid-world commands.
23
23
 
@@ -32,6 +32,7 @@ Slopcamera installs from its verified release archive or from source. Historical
32
32
  | Raster artwork converted to SVG | [Vectorization](references/vectorization.md) |
33
33
  | Clean up, caption, frame, or deliver existing recordings and footage | [Video projects](references/video-projects.md) |
34
34
  | Music video, dancing mascot or timed HTML scene with a local soundtrack | [Music videos](references/music-video.md) |
35
+ | Product launch film, UI mockup motion, kinetic type, or coming from HyperFrames or Remotion | [Launch films](references/launch-films.md); for a whole Hraness product launch, the `product-launch` agent skill |
35
36
  | Authorized YouTube or other public web-media excerpt (`yt-dlp` / `youtube-dl`) | [Web-media excerpts](references/web-media-excerpts.md) |
36
37
  | Generate images, clips, narration or transcripts | [Gateway media](references/gateway-media.md) |
37
38
  | Product mark, marketing illustration or topic icon | [Brand illustrations](references/brand-illustrations.md) |
@@ -1,4 +1,4 @@
1
- # Install and diagnose Slopcamera
1
+ # Install and diagnose SlopCamera
2
2
 
3
3
  Use this reference when the CLI is unavailable, a command differs from the installed release, or `slopcamera doctor` reports a missing dependency for the selected workflow.
4
4
 
@@ -14,14 +14,14 @@ command -v slopcamera
14
14
 
15
15
  A restricted shell can omit package-manager paths. Check known host installation paths before declaring a tool unavailable. If Bun is genuinely absent, follow its official [installation guide](https://bun.sh/docs/installation) within the user’s authorized setup scope. Do not switch package managers or pipe an unreviewed installer into a shell.
16
16
 
17
- Slopcamera installs from its verified release archive: `bun add --global https://github.com/hraness/slopcamera/releases/download/v3.5.0/hraness-slopcamera-3.5.0.tgz`, then `slopcamera doctor --json`. Building from source is the contributor path. Historical Atet archives do not install the renamed CLI; never substitute `slopcamera` into an old archive URL.
17
+ Slopcamera installs from its verified release archive: `bun add --global https://github.com/hraness/slopcamera/releases/download/v3.6.0/hraness-slopcamera-3.6.0.tgz`, then `slopcamera doctor --json`. Building from source is the contributor path. Historical Atet archives do not install the renamed CLI; never substitute `slopcamera` into an old archive URL.
18
18
 
19
19
  If an update from a previous local archive reports `DependencyLoop`, use Bun's
20
20
  [named tarball syntax](https://bun.com/docs/pm/cli/add#tarball-dependencies) with
21
21
  the same verified archive:
22
22
 
23
23
  ```sh
24
- bun add --global @hraness/slopcamera@https://github.com/hraness/slopcamera/releases/download/v3.5.0/hraness-slopcamera-3.5.0.tgz
24
+ bun add --global @hraness/slopcamera@https://github.com/hraness/slopcamera/releases/download/v3.6.0/hraness-slopcamera-3.6.0.tgz
25
25
  slopcamera --version
26
26
  ```
27
27
 
@@ -0,0 +1,158 @@
1
+ # Make a product launch film
2
+
3
+ Use this workflow for a short film that walks through a product: kinetic type,
4
+ the product's own UI in a browser frame with a moving camera, a cursor and a
5
+ drawn highlight, a proof act with real numbers, a limits card and an end card.
6
+ The film is plain HTML, CSS and JavaScript rendered by `slopcamera html render`.
7
+ React is used only to turn the site's mockup components into static markup.
8
+
9
+ The same source gives a web MP4 and WebM, a poster, a social still, square and
10
+ vertical cuts, one short clip per act, and WebVTT captions. Each act's caption
11
+ doubles as the source text for a social thread post.
12
+
13
+ On Hraness product sites the film is one step of the `product-launch` agent
14
+ skill, which also covers the mockups, the launch post and the social kit. Use
15
+ that skill for the whole launch and this page for the film.
16
+
17
+ ## Check the command
18
+
19
+ ```sh
20
+ slopcamera help html
21
+ slopcamera doctor --json
22
+ ```
23
+
24
+ The installed CLI must list `html init`, `html still` and `html deliver`.
25
+ Rendering needs the admitted local Chrome runtime and FFmpeg/FFprobe. Follow
26
+ [installation and readiness](install.md) if the command is missing.
27
+
28
+ ## Start a film
29
+
30
+ ```sh
31
+ slopcamera html init video --template launch-film --aspect 16:9
32
+ cd video
33
+ bun install
34
+ bun run build
35
+ ```
36
+
37
+ `--aspect` takes `16:9`, `1:1` or `9:16` and only sets the default in
38
+ `film.json`; `bun build.ts --aspect 9:16` re-lays the same film at another size.
39
+ The project pins the SlopCamera version that wrote it in `package.json`, so the
40
+ CLI version is recorded next to the source.
41
+
42
+ | File | Edit it to |
43
+ | --- | --- |
44
+ | `film.json` | Change copy, steps, proof items, colors, fonts and product CSS. |
45
+ | `mockups.tsx` | Replace the placeholder product with the site's real mockup components. |
46
+ | `timeline.ts` | Change act order and length. Captions and clips follow it. |
47
+ | `film.css` | Restyle the stage, masks, grain and browser frame. |
48
+ | `film.js` | Change the choreography. Keep every value a function of `t`. |
49
+
50
+ `bun run build` writes `out/film.html`, `out/scene.json`, `out/captions.vtt`
51
+ and `out/beats.json`. It inlines `@hraness/design-kit` `mockups.css` when the
52
+ project can resolve it, then any `productCss` files and fonts, and validates the
53
+ scene before writing it. The document limit is 1 MiB; the build prints its size.
54
+
55
+ ## Keep every frame a function of time
56
+
57
+ A renderer can ask for any frame in any order, once or twice. Build each frame
58
+ from `t` alone:
59
+
60
+ - Read time from `SlopcameraOverlay.onFrame(({ timeMs }) => ...)`. Never use
61
+ `Date.now`, `requestAnimationFrame`, CSS transitions, CSS animations or a
62
+ counter carried between frames.
63
+ - Hide finished scenes with `show(element, on)`. It sets both `visibility` and
64
+ `display`, so a hidden scene cannot leak a stale layer into a later frame.
65
+ - Measure layout once, before the first frame, and store the rectangles. Do not
66
+ measure inside a scaled or transformed scene.
67
+ - Load fonts and images through `SlopcameraOverlay.ready(...)` before frames.
68
+
69
+ The helpers in `@hraness/slopcamera/local/html-film` follow these rules.
70
+ `build.ts` bundles them into `out/film.html`.
71
+
72
+ | Need | Helpers |
73
+ | --- | --- |
74
+ | Numbers and easing | `clamp`, `lerp`, `prog(t, start, end)`, `easings`, `spring` |
75
+ | Kinetic type | `split(element, "word" \| "char")`, then `kin(element, t, inAt, outAt)` |
76
+ | Acts and state | `defineTimeline`, `crossfade`, `stepValue`, `show` |
77
+ | Product walk | `camera`, `cameraBetween`, `cameraTransform`, `cursor`, `placeCursor`, `drawMark` |
78
+ | Captions | `captionsFromTimeline(timeline)` writes WebVTT from act captions |
79
+
80
+ ## Direct each act
81
+
82
+ - **Cold open.** Two short lines over a collage of the problem. Stagger words
83
+ by 60 to 90 ms and hold each line long enough to read twice.
84
+ - **Title.** The product name and the same one-line promise the product's site
85
+ uses. One idea, one hold.
86
+ - **Product walk.** One step per entry in `film.json`. Each step names a
87
+ `data-film` surface for the camera `focus`, a `target` for the cursor click,
88
+ an optional `highlight` outline and an optional `after` state set on the target
89
+ once clicked. Let the camera settle before the cursor moves, click, then draw
90
+ the highlight. Keep steps near 4 seconds.
91
+ - **Proof.** Count numbers up from a facts file or release record. Never type a
92
+ number into `film.json` by hand.
93
+ - **Limits.** Say plainly what the product does not do.
94
+ - **End card.** Name, address and one line.
95
+
96
+ Keep public copy in plain words and sentence case, with no exclamation marks and
97
+ no hype. Keep the placeholder illustration note until the surfaces show the real
98
+ product.
99
+
100
+ ## Review stills before rendering
101
+
102
+ ```sh
103
+ slopcamera html still --input out/scene.json --at 3,12.5,20 --output out/stills
104
+ slopcamera html preview --input out/scene.json --every 2 --output out/preview
105
+ ```
106
+
107
+ Both render with the same injected runtime as `html render`, so a still matches
108
+ the rendered frame at that time. `preview` also writes `contact-sheet.png`. Look
109
+ at each act's hold, each click and each highlight before a full render. For a
110
+ quick draft render, `bun build.ts --scale 0.5 --fps 15 --until 3` keeps the
111
+ layout and shrinks the canvas, frame rate and length.
112
+
113
+ ## Render and deliver
114
+
115
+ ```sh
116
+ slopcamera html render --input out/scene.json --json > out/export.json
117
+ slopcamera html deliver out/export.json --basename launch --poster-at 9 --social-at 9 \
118
+ --cuts 1:1,9:16 --per-beat-clips
119
+ ```
120
+
121
+ `deliver` reads `output.path` from the export and checks its hash, then writes
122
+ into `out/deliver/`:
123
+
124
+ - `launch.mp4`, H.264 with faststart for the web, and `launch.webm`, VP9
125
+ - `launch-poster.jpg` and a 1200x630 `launch-social.jpg`
126
+ - `launch-1x1.mp4` and `launch-9x16.mp4`, the whole frame over a blurred fill
127
+ - one 6 to 10 second clip per act from `beats.json` beside the export, or the file `--beats` names
128
+ - `launch-receipt.json` with each file's size, hash and budget
129
+
130
+ Budgets are 12 MB per MP4, 10 MB for the WebM and 250 KB per JPEG. `deliver`
131
+ exits non-zero when a file is over budget; shorten the film, simplify the grain
132
+ or lower the frame rate rather than raising the budget.
133
+
134
+ ## Turn beats into a thread
135
+
136
+ `out/beats.json` lists each act's id, start, end and caption. Use one post per
137
+ act, in order, with that act's clip or still attached. The captions are already
138
+ short and in plain words; edit them in `film.json` so the film and the posts stay
139
+ the same. The WebVTT file carries the same lines for the web player.
140
+
141
+ ## Coming from HyperFrames or Remotion
142
+
143
+ There is no importer. The ideas map directly:
144
+
145
+ | HyperFrames or Remotion | Launch film |
146
+ | --- | --- |
147
+ | A composition's HTML, CSS and JS | `film.html`, `film.css` and `film.js` in a `plain` scene |
148
+ | `useCurrentFrame()` | `timeMs` from `SlopcameraOverlay.onFrame`, as `t` in seconds |
149
+ | `interpolate(frame, [a, b], [x, y])` | `lerp(x, y, prog(t, a, b))` |
150
+ | `spring({ frame, fps })` | `spring(seconds)` |
151
+ | `<Sequence from durationInFrames>` | an act in `defineTimeline`, read with `timeline.local(t, id)` |
152
+ | `<Series>` with overlaps | acts with `overlap`, blended by `crossfade` |
153
+ | `staticFile()` | a declared resource read with `SlopcameraOverlay.asset(name)` |
154
+ | `delayRender()` | `SlopcameraOverlay.ready(promise)` |
155
+ | Rendering with the CLI | `slopcamera html render`, then `html deliver` |
156
+
157
+ Keep React components as mockups rendered to static markup. Do not run React
158
+ inside the film: every frame must come from `t`, not from component state.
@@ -1,7 +1,7 @@
1
1
  # Reference-led Three.js and metallic treatments
2
2
 
3
3
  Use this guide only when Slopcamera's complete local Code Mode host is available.
4
- It ships in this repository alongside the local CLI host and optional unbundled menu-bar companion. The
4
+ It ships in this repository alongside the local CLI host. The
5
5
  portable `@hraness/slopcamera/code` and `@hraness/slopcamera/workflow`
6
6
  entrypoints intentionally expose no HTML renderer, Three.js operation, or
7
7
  arbitrary source execution. Those capabilities belong to the local host.
@@ -1,4 +1,4 @@
1
- # Slopcamera visual communication rules
1
+ # SlopCamera visual communication rules
2
2
 
3
3
  Use these rules to turn supplied content into a diagram without adding content.
4
4
 
package/src/cli.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { readFile, writeFile } from "node:fs/promises"
4
- import { resolve } from "node:path"
4
+ import { dirname, resolve } from "node:path"
5
5
  import {
6
6
  artifactSummary,
7
7
  checkDiagramFile,
@@ -17,8 +17,16 @@ import {
17
17
  } from "./generate.js"
18
18
  import {
19
19
  generateSlopcameraIcon,
20
+ slopcameraIconContexts,
20
21
  slopcameraIconMaximumRounds,
22
+ type SlopcameraIconContext,
23
+ type SlopcameraIconPurpose,
21
24
  } from "./icon.js"
25
+ import {
26
+ generateSlopcameraIconSet,
27
+ parseSlopcameraIconSetSpec,
28
+ slopcameraIconSetMaximumSetRounds,
29
+ } from "./icon-set.js"
22
30
  import {
23
31
  generateSlopcameraImageGallery,
24
32
  parseSlopcameraGalleryVary,
@@ -59,7 +67,11 @@ Usage:
59
67
  slopcamera image generate <prompt> --output <file.png|jpg|webp> [--model <provider/model>] [--json]
60
68
  slopcamera image icon <subject> --output <file.svg> [--purpose <mark|illustration>]
61
69
  [--model <provider/model>] [--ink <#rgb|#rrggbb>] [--rounds <1-${slopcameraIconMaximumRounds}>]
70
+ [--context <${slopcameraIconContexts.join("|")}>] [--candidates <1-${slopcameraIconMaximumRounds}>]
62
71
  [--critique-model <provider/model>] [--keep-raster] [--json]
72
+ slopcamera image icon --set <file.json> --output-dir <directory>
73
+ [--context <${slopcameraIconContexts.join("|")}>] [--candidates <1-${slopcameraIconMaximumRounds}>]
74
+ [--set-rounds <1-${slopcameraIconSetMaximumSetRounds}>] [--keep-raster] [--json]
63
75
  slopcamera image gallery <subject> --output-dir <directory> [--kind <${slopcameraGalleryKinds.join("|")}>]
64
76
  [--count <1-${slopcameraGalleryLimits.candidates}>] [--vary <axis[=v1,v2][;axis...]>] [--candidates <file.json>]
65
77
  [--model <provider/model>] [--cell <${slopcameraGalleryLimits.cellEdgeMin}-${slopcameraGalleryLimits.cellEdgeMax}>] [--tile|--no-tile] [--json]
@@ -103,6 +115,13 @@ vectorizer, and (when --rounds exceeds 1) critiqued by a vision model whose
103
115
  feedback revises the prompt for the next attempt. Only Slopcamera's own
104
116
  generated output is uploaded for critique — never user media.
105
117
 
118
+ Icon --set reads a bounded JSON manifest ({name?, ink?, context?, members:
119
+ [{slug, subject, purpose?, context?}]}) and generates the whole family at
120
+ once: each member keeps a small candidate pool, joint selection minimizes the
121
+ measured coverage and stroke-weight spread across the set, and a
122
+ contact-sheet critique reviews the family as a unit before SVGs publish.
123
+ Manifests and receipts stay local.
124
+
106
125
  Gallery generates several bounded candidates in parallel and composes them
107
126
  into one labelled contact sheet plus a receipt. Use it to review texture,
108
127
  skybox, backdrop, sprite, or design alternatives, then promote a chosen
@@ -250,6 +269,7 @@ export interface SlopcameraCliDependencies {
250
269
  readonly generate?: typeof generateSlopcameraImageFile
251
270
  readonly hostResourceCoordinator?: HostResourceCoordinator
252
271
  readonly icon?: typeof generateSlopcameraIcon
272
+ readonly iconSet?: typeof generateSlopcameraIconSet
253
273
  readonly log?: (value: string) => void
254
274
  readonly vectorize?: typeof vectorizeImage
255
275
  }
@@ -565,7 +585,19 @@ export async function main(
565
585
  if (command === "icon") {
566
586
  const parsed = parseArguments(
567
587
  rest,
568
- new Set(["model", "output", "ink", "purpose", "rounds", "critique-model"]),
588
+ new Set([
589
+ "model",
590
+ "output",
591
+ "output-dir",
592
+ "ink",
593
+ "purpose",
594
+ "rounds",
595
+ "critique-model",
596
+ "context",
597
+ "candidates",
598
+ "set",
599
+ "set-rounds",
600
+ ]),
569
601
  )
570
602
  const unknownFlags = [...parsed.flags].filter(
571
603
  (flag) => flag !== "json" && flag !== "keep-raster",
@@ -573,13 +605,6 @@ export async function main(
573
605
  if (unknownFlags.length > 0) {
574
606
  throw new Error(`Unknown icon option: --${unknownFlags[0]}`)
575
607
  }
576
- if (parsed.positionals.length !== 1) {
577
- throw new Error("slopcamera image icon accepts exactly one subject")
578
- }
579
- const output = requiredOption(parsed, "output")
580
- if (!output.toLowerCase().endsWith(".svg")) {
581
- throw new Error("--output must end in .svg")
582
- }
583
608
  const model = parsed.options.model ?? slopcameraImageModels[1]
584
609
  if (
585
610
  model.length > 256 ||
@@ -607,10 +632,97 @@ export async function main(
607
632
  if (purpose !== undefined && purpose !== "mark" && purpose !== "illustration") {
608
633
  throw new Error("--purpose must be mark or illustration")
609
634
  }
635
+ const context = parsed.options.context
636
+ if (
637
+ context !== undefined &&
638
+ !slopcameraIconContexts.includes(context as never)
639
+ ) {
640
+ throw new Error(
641
+ `--context must be one of: ${slopcameraIconContexts.join(", ")}`,
642
+ )
643
+ }
610
644
  const rounds = parsePositiveInteger(parsed.options.rounds, "rounds")
611
645
  if (rounds !== undefined && rounds > slopcameraIconMaximumRounds) {
612
646
  throw new Error(`--rounds must be at most ${slopcameraIconMaximumRounds}`)
613
647
  }
648
+ const candidates = parsePositiveInteger(parsed.options.candidates, "candidates")
649
+ if (candidates !== undefined && candidates > slopcameraIconMaximumRounds) {
650
+ throw new Error(`--candidates must be at most ${slopcameraIconMaximumRounds}`)
651
+ }
652
+ const setRounds = parsePositiveInteger(parsed.options["set-rounds"], "set-rounds")
653
+ if (setRounds !== undefined && setRounds > slopcameraIconSetMaximumSetRounds) {
654
+ throw new Error(`--set-rounds must be at most ${slopcameraIconSetMaximumSetRounds}`)
655
+ }
656
+
657
+ if (parsed.options.set !== undefined) {
658
+ if (parsed.positionals.length !== 0) {
659
+ throw new Error("slopcamera image icon --set takes no subject positional")
660
+ }
661
+ if (parsed.options.output !== undefined) {
662
+ throw new Error("--output cannot be combined with --set; use --output-dir")
663
+ }
664
+ const outputDir = requiredOption(parsed, "output-dir")
665
+ const manifestPath = parsed.options.set
666
+ let manifestText: string
667
+ try {
668
+ manifestText = await readFile(manifestPath, "utf8")
669
+ } catch {
670
+ throw new Error(`--set manifest could not be read: ${manifestPath}`)
671
+ }
672
+ if (Buffer.byteLength(manifestText, "utf8") > 256 * 1024) {
673
+ throw new Error("--set manifest exceeds the 256 KiB bound")
674
+ }
675
+ let manifestJson: unknown
676
+ try {
677
+ manifestJson = JSON.parse(manifestText)
678
+ } catch {
679
+ throw new Error("--set manifest must be valid JSON")
680
+ }
681
+ const spec = parseSlopcameraIconSetSpec(manifestJson)
682
+ const result = await withSlopcameraOperationHostAdmission(
683
+ "slopcamera.image.icon",
684
+ async (lease) =>
685
+ await (dependencies.iconSet ?? generateSlopcameraIconSet)({
686
+ keepRaster: parsed.flags.has("keep-raster"),
687
+ manifestDir: dirname(resolve(manifestPath)),
688
+ model,
689
+ outputDir,
690
+ spec,
691
+ inheritedFileDescriptors: [lease.inheritedFileDescriptor],
692
+ ...(critiqueModel === undefined ? {} : { critiqueModel }),
693
+ ...(ink === undefined ? {} : { ink }),
694
+ ...(context === undefined
695
+ ? {}
696
+ : { context: context as SlopcameraIconContext }),
697
+ ...(purpose === undefined
698
+ ? {}
699
+ : { purpose: purpose as SlopcameraIconPurpose }),
700
+ ...(rounds === undefined ? {} : { rounds }),
701
+ ...(candidates === undefined ? {} : { candidatesPerMember: candidates }),
702
+ ...(setRounds === undefined ? {} : { setRounds }),
703
+ }),
704
+ hostAdmissionOptions(dependencies),
705
+ )
706
+ if (parsed.flags.has("json")) {
707
+ ;(dependencies.log ?? console.log)(JSON.stringify(result, null, 2))
708
+ } else {
709
+ ;(dependencies.log ?? console.log)(
710
+ `Icon set ${result.name ?? "icon-set"}: ${result.members.length} members to ${result.outputDir} ` +
711
+ `(target coverage ${result.target.coverageRatio.toFixed(3)}, ` +
712
+ `stroke ${result.target.strokePx.toFixed(1)}px, ${result.setRoundsUsed} set rounds)`,
713
+ )
714
+ }
715
+ reportUsefulResult(dependencies.onUsefulResult)
716
+ return
717
+ }
718
+
719
+ if (parsed.positionals.length !== 1) {
720
+ throw new Error("slopcamera image icon accepts exactly one subject")
721
+ }
722
+ const output = requiredOption(parsed, "output")
723
+ if (!output.toLowerCase().endsWith(".svg")) {
724
+ throw new Error("--output must end in .svg")
725
+ }
614
726
  const result = await withSlopcameraOperationHostAdmission(
615
727
  "slopcamera.image.icon",
616
728
  async (lease) => await (dependencies.icon ?? generateSlopcameraIcon)({
@@ -623,6 +735,10 @@ export async function main(
623
735
  ...(ink === undefined ? {} : { ink }),
624
736
  ...(purpose === undefined ? {} : { purpose }),
625
737
  ...(rounds === undefined ? {} : { rounds }),
738
+ ...(context === undefined
739
+ ? {}
740
+ : { context: context as SlopcameraIconContext }),
741
+ ...(candidates === undefined ? {} : { candidatePool: candidates }),
626
742
  }),
627
743
  hostAdmissionOptions(dependencies),
628
744
  )
@@ -53,7 +53,7 @@ interface ImagePricedModel {
53
53
 
54
54
  export type SlopcameraImageModelPrice = TokenPricedImageModel | ImagePricedModel
55
55
 
56
- export const slopcameraProviderPricesCheckedAt = "2026-09-26"
56
+ export const slopcameraProviderPricesCheckedAt = "2026-09-27"
57
57
 
58
58
  /**
59
59
  * Cited list prices. A model without an entry here cannot be billed: the
@@ -88,6 +88,26 @@ export const slopcameraImageModelPrices: Readonly<
88
88
  perImage: 35_000,
89
89
  source: "https://vercel.com/ai-gateway/models/recraft-v4.1-utility",
90
90
  }),
91
+ "bfl/flux-pro-1.1": Object.freeze({
92
+ kind: "image",
93
+ perImage: 40_000,
94
+ source: "https://vercel.com/ai-gateway/models/flux-pro-1.1",
95
+ }),
96
+ "bfl/flux-pro-1.1-ultra": Object.freeze({
97
+ kind: "image",
98
+ perImage: 60_000,
99
+ source: "https://vercel.com/ai-gateway/models/flux-pro-1.1-ultra",
100
+ }),
101
+ "bfl/flux-kontext-pro": Object.freeze({
102
+ kind: "image",
103
+ perImage: 40_000,
104
+ source: "https://vercel.com/ai-gateway/models/flux-kontext-pro",
105
+ }),
106
+ "bfl/flux-kontext-max": Object.freeze({
107
+ kind: "image",
108
+ perImage: 80_000,
109
+ source: "https://vercel.com/ai-gateway/models/flux-kontext-max",
110
+ }),
91
111
  })
92
112
 
93
113
  /**