pixelkiln 0.87.0 → 0.89.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.
package/docs/CLI.md CHANGED
@@ -54,6 +54,8 @@ Which command:
54
54
  | see the account balance | `pixelkiln balance` |
55
55
  | shrink upscaled outside art to its native grid before using it as a reference | `pixelkiln unzoom --from refs/knight-512.png --out refs/knight.png` |
56
56
  | turn a photo, painting, or render into pixel art | `pixelkiln pixelate --from renders/lodge.png --out art/lodge.png` |
57
+ | make a normal map so an engine can light a sprite or a loop strip | `pixelkiln normal-map --from art/hero-walk.png --frames 6` |
58
+ | make a pixel-art effect (spell, hit, explosion, aura), free | `pixelkiln vfx --preset explosion --out fx/explosion` |
57
59
  | generate a pixel font (`.ttf` plus glyph atlas) | `pixelkiln font --description "warm orange arcade font" --out fonts/arcade` |
58
60
  | start an `animate-skeleton` keypoints file from an image | `pixelkiln estimate-skeleton poses/hero.png --out poses/hero-swing.json` |
59
61
  | see a keypoints file's poses over the source before paying | `pixelkiln skeleton-preview hero-swing` |
@@ -1007,8 +1009,9 @@ which recovers the grid instead of redrawing it.
1007
1009
  - PixelLab picks the output size from the source's detected pixel scale; there
1008
1010
  is no size option. A 256x256 render came back 108x109 and a 384x384 render
1009
1011
  84x110. Sources over 2048px on the longer side are scaled down upstream.
1010
- - The result keeps transparency: both opaque renders measured came back with
1011
- the subject cut out.
1012
+ - **The result is opaque**, on a near-white background (transparent source
1013
+ areas become white too), like `unzoom`. Remove the background afterwards
1014
+ when you need a cut-out sprite.
1012
1015
  - `--description` optionally steers the style, e.g. `"retro vibes with light
1013
1016
  dithering"`.
1014
1017
  - A flat **6 generations**, confirmed against a live bill. The command prints
@@ -1023,6 +1026,130 @@ Like `unzoom`, it writes a loose file and leaves the lockfile alone. Declare
1023
1026
  the result as an asset's `source`, or use it as a `styleImages` or
1024
1027
  `reference` path, to bring it into a manifest.
1025
1028
 
1029
+ ### `normal-map`
1030
+
1031
+ ```bash
1032
+ pixelkiln normal-map --from art/hero-walk.png --frames 6 --convention opengl --out art/hero-walk-normal.png
1033
+ ```
1034
+
1035
+ Makes the normal map of a sprite with PixelLab's `/normal-map`, so a game
1036
+ engine can light it. The map is the input's size: RGB is the way each pixel
1037
+ faces, and alpha is 255 where the sprite shows and 0 elsewhere.
1038
+
1039
+ - `--from` must be a PNG: its alpha marks where the sprite is. For an
1040
+ animation, pass a horizontal strip of equal frames and `--frames <n>` (1–32,
1041
+ default 1). PixelLab shapes the first frame and carries that to the others,
1042
+ so the light stays steady, and returns the map as a strip of the same
1043
+ layout.
1044
+ - Each frame must be 8 to 512 pixels per side, and the strip's width must
1045
+ divide evenly by `--frames`. Both are checked before anything is sent.
1046
+ - `--convention opengl` (the default) is green up, for Godot, Unity, and
1047
+ Phaser; `--convention directx` is green down, for Unreal.
1048
+ - **0.5 generation** per call, a whole strip included, charged only when the
1049
+ map is delivered (PixelLab's documented price; not yet measured here). The
1050
+ command prints that and asks first; `--yes` skips the question,
1051
+ `--budget <n>` refuses when `n` is below it, and `--dry-run` sends nothing.
1052
+ - `--out` defaults to `<input>.normal.png` beside the input, and the input is
1053
+ never overwritten. An existing `--out` is replaced only with `--force`.
1054
+ - `--json` prints the job id, the map's size, the frame count, the convention,
1055
+ and the billed usage.
1056
+
1057
+ It writes a loose file and leaves the lockfile alone. For a manifest asset or
1058
+ a loop, declare a [`normal-map` revision](./REVISIONS.md#normal-maps) instead:
1059
+ it tracks the parent and cuts a loop's map back into per-frame files.
1060
+
1061
+ ### `vfx`
1062
+
1063
+ ```bash
1064
+ pixelkiln vfx presets
1065
+ pixelkiln vfx parts
1066
+ pixelkiln vfx --preset explosion --part scorch_mark --out fx/explosion --game-pack
1067
+ pixelkiln vfx --preset heal_pillar --sprite sprites/hero.png --out fx/hero-heal
1068
+ pixelkiln vfx --recipe fx/explosion.vfx.json --out fx/explosion-v2
1069
+ pixelkiln vfx --effect <saved effect id> --out fx/explosion --force
1070
+ ```
1071
+
1072
+ Builds an animated pixel-art effect with PixelLab's VFX workbench (Create
1073
+ VFX), exports it, and downloads it. PixelLab only offers the workbench as an
1074
+ MCP tool, with no REST endpoint, so this command talks to PixelLab's MCP
1075
+ server with the same `PIXELLAB_API_KEY`. The workbench is a renderer, not a
1076
+ model: it is **free** on a subscription and the same recipe renders the
1077
+ same pixels every time.
1078
+
1079
+ - `vfx presets` and `vfx parts` print PixelLab's current lists: whole-effect
1080
+ presets (explosion, lightning_strike, heal_pillar, portal, aura, and more)
1081
+ and parts to layer on (scorch_mark, spark_burst, stun_stars, rain_streaks,
1082
+ and about 90 others).
1083
+ - Start from exactly one of `--preset <name>`, `--recipe <file>`, or
1084
+ `--effect <id>`, or from `--part` alone. `--part <name>` is repeatable and
1085
+ adds parts on top, in order.
1086
+ - `--sprite` builds the effect around a character: a local PNG or JPEG, a
1087
+ URL, or a PixelLab reference such as `character:<id>:south`.
1088
+ - `--fps <n>` sets the frame rate (1–60). `--name` sets the name saved to
1089
+ your PixelLab "My effects"; it defaults to the preset or the output's file
1090
+ name.
1091
+ - `--out <base>` writes `<base>.png` (spritesheet), `<base>.gif`,
1092
+ `<base>.json` (an Aseprite-style frame atlas pointing at the sheet, with the
1093
+ effect's anchor in `meta.anchor`), and `<base>.vfx.json`. `--game-pack`
1094
+ adds `<base>-game.zip`: every piece as its own clip (the whole effect, the
1095
+ layers behind and in front of the sprite, each part) plus a
1096
+ `manifest.json` saying where each plays. Existing files are replaced only
1097
+ with `--force`.
1098
+ - `<base>.vfx.json` records the saved effect's id, its edit link on
1099
+ pixellab.ai, the frame facts, the sprite it was built around, and the
1100
+ effect's full **recipe**. Pass it back as `--recipe` to render the same
1101
+ effect again; it reuses the recorded sprite unless you pass `--sprite`.
1102
+ - Every run saves the effect to your My effects, where its sliders can be
1103
+ moved on pixellab.ai. Pick the changes up with `--effect <id> --force`,
1104
+ which exports over the saved effect and keeps its name.
1105
+ - `--json` prints the export facts and paths; `--dry-run` sends nothing.
1106
+
1107
+ For effects that need more than presets and parts (your own layers, exact
1108
+ timing, a projectile you drew), use the `vfx_workbench` tool from an MCP
1109
+ client such as Claude Code. Then pull the result into the project with
1110
+ `pixelkiln vfx --effect <id>`.
1111
+
1112
+ #### Described effects (paid)
1113
+
1114
+ ```bash
1115
+ pixelkiln vfx --description "a blue lightning strike that leaves sparks" --out fx/zap
1116
+ pixelkiln vfx --description "a green healing aura" --artist pro --sprite sprites/hero.png --out fx/heal
1117
+ pixelkiln vfx --description "make it red and faster" --effect <effect id> --out fx/zap --force
1118
+ ```
1119
+
1120
+ `--description` asks PixelLab's VFX artist (`POST /vfx`) to build the
1121
+ effect from words instead of the free workbench: it picks the parts, renders
1122
+ the effect, checks it, and saves it to My effects. It goes over the REST API
1123
+ and needs no MCP.
1124
+
1125
+ - `--artist lite` (the default) or `--artist pro`, the stronger artist for
1126
+ detailed effects.
1127
+ - `--sprite` must be a local PNG up to 512×512; the artist takes pixels, not
1128
+ a URL or a `character:` reference. `--preset`, `--recipe`, `--part`, and
1129
+ `--fps` are refused with `--description`.
1130
+ - `--effect <id>` changes that saved effect in place, with `--description`
1131
+ saying what to change. When `<base>.vfx.json` is that effect's record from
1132
+ an earlier run, its `conversation` is sent too, which PixelLab prices as a
1133
+ change. Without one, PixelLab prices it as a new effect.
1134
+ - **Cost.** PixelLab quotes each job and bills on delivery, up to twice the
1135
+ quote when the effect runs long. Its table: lite 1 generation (3 with a
1136
+ sprite over 128 px, 5 over 256 px; a change 1–2), pro 6 (8 with a sprite,
1137
+ 14 over 128 px, 28 over 256 px; a change 4–8). The command prints the quote
1138
+ and the twice-the-quote ceiling and asks first. `--budget <n>` refuses when
1139
+ `n` is below the ceiling, `--yes` skips the question, and `--dry-run` sends
1140
+ nothing. A change prices the saved effect's sprite at the top tier unless
1141
+ you pass `--sprite`, since its size is not known here.
1142
+ - It writes the same `<base>.png`, `.gif`, `.json`, and `-game.zip` (with
1143
+ `--game-pack`) as the workbench, plus `<base>-normals.png` when a light in
1144
+ the effect made the sprite's normal map. `<base>.vfx.json` records the
1145
+ effect id, edit link, the artist's message, the frame facts, the quote, the
1146
+ bill, and the `conversation`. It holds no recipe; to get a free, repeatable
1147
+ one, run `pixelkiln vfx --effect <id> --out <another base>` afterwards.
1148
+
1149
+ To plan, lock, and pack effects with the rest of a project's art, declare
1150
+ them in the manifest instead, with the [`vfx` generator](./GENERATORS.md#vfx).
1151
+ A `.vfx.json` record from this command works there as a `recipe`.
1152
+
1026
1153
  ### `font`
1027
1154
 
1028
1155
  ```bash
@@ -1106,14 +1233,15 @@ Print the package version. `-v` is an alias.
1106
1233
  | Option | Applies to | Meaning |
1107
1234
  |---|---|---|
1108
1235
  | `--manifest <path>` | manifest commands | Manifest path; defaults to `pixelkiln.manifest.json`. |
1109
- | `--frames <n>` | `estimate-skeleton` | Frames to scaffold after the estimated pose, 3–15; default 4. |
1236
+ | `--frames <n>` | `estimate-skeleton`/normal-map | Frames to scaffold after the estimated pose, 3–15; default 4. For normal-map, the equal frames in the `--from` strip, 1–32; default 1. |
1237
+ | `--convention <c>` | normal-map | `opengl` (green up, the default) or `directx` (green down, for Unreal). |
1110
1238
  | `--lock <path>` | manifest commands | Lock path; defaults beside the manifest. |
1111
1239
  | `--style a,b` | most workflows | Restrict styles; repeatable. |
1112
1240
  | `--only id1,id2` | most workflows | Restrict asset ids; repeatable. An `animation.directions` shorthand id selects every loop and mirror it expands into. |
1113
1241
  | `--budget <n\|provider=n>` | submit/gen/gallery | Refuse work above this cost. Repeat `provider=n` for every provider in a mixed run; do not mix keyed and unkeyed forms. For `gallery`, the session ceiling that enables generation from the page. |
1114
1242
  | `--force` | gen/fetch/derived commands/recipe install/quality snapshot | Regenerate current work, replace a changed or untracked fetch destination, rebuild current quality-profile output, take ownership of modified/unowned derived output, replace changed recipe files, or replace a changed quality baseline. A refinement rebuild resets approval. |
1115
1243
  | `--dry-run` | supported mutating commands | Inspect without spending or mutating provider state. |
1116
- | `--json` | plan/doctor/audit/cache/status/gallery/salvage/refine/recipe/quality | Machine-readable stdout where supported. For `gallery`, prints the snapshot instead of serving it. |
1244
+ | `--json` | plan/doctor/audit/cache/status/gallery/salvage/refine/recipe/quality/unzoom/pixelate/normal-map/vfx/font | Machine-readable stdout where supported. For `gallery`, prints the snapshot instead of serving it. |
1117
1245
  | `--check` | plan/audit/cache | Exit nonzero when selected state is unsafe. |
1118
1246
  | `--yes`, `-y` | confirmed operations | Skip an interactive confirmation. For `refine approve`, it records an already-completed human review; it does not replace one. |
1119
1247
  | `--no-open` | pick/salvage/gallery/edit | Do not automatically open the browser (`open`, `xdg-open`, or `start`, by platform), or the editor for `edit`. |
@@ -1126,13 +1254,13 @@ Print the package version. `-v` is an alias.
1126
1254
  | `--provider <id>` | balance/adopt/salvage/purge/workspace add | Select the account provider for a mixed manifest, or set the workspace catalog's default provider hint. |
1127
1255
  | `--account <label>` | workspace add | Free-form account label, e.g. distinguishing sandboxes. |
1128
1256
  | `--all` | salvage dry run | List every unclaimed object rather than the first 30. |
1129
- | `--from <path>` | init/refine/quality check/unzoom | Existing source tree for init; source PNG or quality record for one-file refine mode; baseline for quality check; the upscaled image for unzoom; the photo or render for pixelate. Omit it to use manifest `style.quality`. |
1257
+ | `--from <path>` | init/refine/quality check/unzoom/pixelate/normal-map | Existing source tree for init; source PNG or quality record for one-file refine mode; baseline for quality check; the upscaled image for unzoom; the photo or render for pixelate; the sprite or strip for normal-map. Omit it to use manifest `style.quality`. |
1130
1258
  | `--exclude <names>` | init | Directory/name fragments to exclude; repeatable. |
1131
1259
  | `--generator <name>` | init | Generator assigned to the scaffolded style. |
1132
- | `--name <name>` | init/font | Project name for the scaffolded manifest, or the generated font's family name. |
1260
+ | `--name <name>` | init/font/vfx | Project name for the scaffolded manifest, the generated font's family name, or the effect's name in My effects. |
1133
1261
  | `--write-prompts` | adopt | Recover provider prompts into the manifest. |
1134
1262
  | `--port <n>` | pick/salvage/gallery | Local review or gallery server port; otherwise chooses a free port. |
1135
- | `--out <path>` | pack/export/refine/recipe install/quality snapshot/unzoom/pixelate/font | Output base override, final native PNG for path-mode refine, exact recipe destination, quality baseline path, the unzoomed or pixelated PNG, or the font's `<base>.ttf`/`<base>.png`. Export requires one selected tileset. |
1263
+ | `--out <path>` | pack/export/refine/recipe install/quality snapshot/unzoom/pixelate/normal-map/vfx/font | Output base override, final native PNG for path-mode refine, exact recipe destination, quality baseline path, the unzoomed, pixelated, or normal-map PNG, the effect's `<base>.png`/`.gif`/`.json`/`.vfx.json`, or the font's `<base>.ttf`/`<base>.png`. Export requires one selected tileset. |
1136
1264
  | `--inputs <path>` | pack/quality snapshot | JSON input array; requires `--out`. Quality cases use `{ id, path, record?, tolerances? }`. |
1137
1265
  | `--columns <n>` | pack/export | Grid columns, 1–1024; default is near-square. |
1138
1266
  | `--format <name>` | export, pack, mount | `export`: `generic` (default), `tiled`, or `godot` (TileSet). `pack` and `mount`: `generic` (default), `aseprite` (sheet JSON), or `godot` (SpriteFrames). |
@@ -1151,9 +1279,17 @@ Print the package version. `-v` is an alias.
1151
1279
  | `--note <text>` | refine approve | Optional review note stored in the quality companion. |
1152
1280
  | `--model-root <path>` | recipe verify | ComfyUI `models` directory. Enables streamed hash checks for every external model declared by the recipe. |
1153
1281
  | `--quantize <n>` | unzoom | `0` auto-detects a palette (default), `-1` keeps every color, `2`-`256` quantizes to exactly that many. |
1154
- | `--description <text>` | font/pixelate | The font's style, e.g. `"warm orange arcade font"`, or an optional style steer for pixelate. |
1282
+ | `--description <text>` | font/pixelate/vfx | The font's style, e.g. `"warm orange arcade font"`, an optional style steer for pixelate, or the effect for PixelLab's paid VFX artist (with `--effect`, what to change). |
1283
+ | `--artist <a>` | vfx | With `--description`: `lite` (default) or `pro`. |
1155
1284
  | `--weight <w>` | font | `Bold` or `Regular` (default). |
1156
1285
  | `--glyph-px <n>` | font | Native glyph size: `8`, `16` (default), `32`, or `64`. |
1286
+ | `--preset <name>` | vfx | Start from one of PixelLab's whole-effect presets; `pixelkiln vfx presets` lists them. |
1287
+ | `--recipe <file>` | vfx | Render a recipe: a bare recipe JSON or a `.vfx.json` record from an earlier run. |
1288
+ | `--effect <id>` | vfx | Start from an effect saved in your PixelLab My effects, and export over it. |
1289
+ | `--part <name>` | vfx | Add a part; repeatable, applied in order. `pixelkiln vfx parts` lists them. |
1290
+ | `--sprite <ref>` | vfx | Build the effect around a sprite: a local PNG/JPEG, a URL, or `character:<id>:<direction>`. |
1291
+ | `--fps <n>` | vfx | Frame rate, 1–60. |
1292
+ | `--game-pack` | vfx | Also download the game-pack ZIP (one clip per piece plus a manifest). |
1157
1293
  | `--generation <n\|hash>` | restore | Bring a replaced generation back: `1` is the most recently replaced, or a prefix of one of its output hashes. One asset at a time. |
1158
1294
 
1159
1295
  ## Exit and output contract
package/docs/ENDPOINTS.md CHANGED
@@ -206,8 +206,8 @@ Measured live, October 2026:
206
206
 
207
207
  | Source | Description | Output | Billed |
208
208
  |---|---|---|---|
209
- | 256×256 opaque render (a knight) | none | 108×109, transparent background | **6** |
210
- | 384×384 opaque render (a mountain keep) | "light dithering" | 84×110, transparent background | **6** |
209
+ | 256×256 opaque render (a knight) | none | 108×109, opaque, near-white background | **6** |
210
+ | 384×384 opaque render (a mountain keep) | "light dithering" | 84×110, opaque, near-white background | **6** |
211
211
 
212
212
  - It is a background job. The submit response carries `estimated_generations:
213
213
  6`, `pricing_provisional: true`, and `image_id`/`source_image_id` (the result
@@ -216,8 +216,10 @@ Measured live, October 2026:
216
216
  image at `last_response.images[0]` as `{type, base64, width, height}`.
217
217
  `last_response.original_images[0]` is the same size, before PixelLab's
218
218
  final "touching up pixels" pass. pixelkiln reads `images[0]`.
219
- - Unlike `image-to-pixelart`, it keeps alpha: both opaque renders came back
220
- with the subject cut out on transparency.
219
+ - **The result is opaque.** Both outputs have every pixel at alpha 255, the
220
+ subject on a near-white field (`#fefefe`). PixelLab's MCP tool for the same
221
+ conversion says transparent source areas become white too. Run
222
+ `remove-background` (1 generation) on the result when you need a cut-out.
221
223
 
222
224
  Wired up as `pixelkiln pixelate`, a standalone command on a loose file like
223
225
  `unzoom`; see [the CLI reference](./CLI.md#pixelate). Use `unzoom` instead
@@ -255,24 +257,164 @@ Re-measured after the cut:
255
257
  `proTierCost` (≤65536px² is 10, ≤102400px² is 15, larger is 25), the same
256
258
  function `inpaint-v3` and `edit-images-v2` revisions use.
257
259
 
258
- **Create VFX** shipped in the same release as an experimental web tool
259
- (`pixellab.ai/create-vfx`). As of October 2026 it has no REST endpoint in
260
- `/v2/openapi.json` and no MCP tool; PixelLab says the MCP tool is coming.
261
- pixelkiln cannot wrap it until one of those exists; see the
262
- [roadmap](../skills/pixelkiln/references/pixellab-roadmap.md).
260
+ ---
261
+
262
+ ## VFX workbench (MCP only), measured
263
+
264
+ Create VFX shipped in PixelLab 0.4.128 as a web tool
265
+ (`pixellab.ai/create-vfx`). Since October 2026 it is also the
266
+ `vfx_workbench` tool on PixelLab's MCP server. The workbench still has **no
267
+ REST endpoint**. 0.4.130 added only the paid artist to REST, as `POST /vfx`
268
+ ([below](#vfx-artist-post-vfx-measured-once)).
269
+
270
+ **It is a renderer, not a model.** An effect is a JSON recipe of layers
271
+ (glows, rings, rays, particles, noise, and more), most of them dropped in as
272
+ tuned "parts" with their own sliders. PixelLab renders the recipe to
273
+ pixel-art frames, generates nothing, and bills nothing on a subscription.
274
+ Rendering is deterministic: the same recipe re-rendered gave a byte-identical
275
+ sheet and GIF.
276
+
277
+ **Calling it.** `POST https://api.pixellab.ai/mcp` with the REST API's own key
278
+ as a bearer token, one JSON-RPC `tools/call` per request. No `initialize`
279
+ handshake or session id is needed (the server issues none). The reply is a
280
+ short server-sent-event stream whose `data:` line holds the JSON-RPC
281
+ response; its `content` is plain text plus PNG contact sheets of the frames.
282
+
283
+ The tool takes one `argv` list per call:
284
+
285
+ - `start [--preset <name> | --effect <saved id>] [--sprite <image>] [--fps N]`
286
+ returns a step id. `--preset` and `--effect` also print the working recipe
287
+ as one JSON line. `--sprite` accepts base64, a `data:` URL, a URL, or
288
+ `character:<id>:<direction>` / `object:<id>` / `gallery:<id>`.
289
+ - `<step> add_part '{"parts": [...]}'`, `set_part`, `remove_part`, `throw`, and
290
+ `render '{"recipe": {...}}'` or `'{"patch": [...]}'` each return a **new**
291
+ step id. Steps are immutable, so an older id is a way back.
292
+ - `<step> export --name <n> [--replace <saved id>] [--game-pack]` saves to
293
+ the account's My effects and replies with the effect id, an edit link,
294
+ `<w>x<h>, <n> frames @ <fps> fps, plays once|loops`, the anchor, and a
295
+ download folder listing `<slug>.gif`, `<slug>.png` (sheet), `<slug>.json`
296
+ (an Aseprite/TexturePacker-style atlas with `meta.anchor`), and with
297
+ `--game-pack`, `<slug>-game.zip` (`whole.png`, one sheet per part,
298
+ `manifest.json` with each clip's depth, frames, and offset). The download
299
+ URLs need no auth.
300
+ - `describe presets|parts|recipe|edits|...` documents the rest: 17 presets
301
+ and about 90 parts as of October 2026.
302
+
303
+ **Two gaps a wrapper has to cover:**
304
+
305
+ - No reply after a part is added prints the recipe, and the exported JSON
306
+ does not carry it. `start --effect <saved id>` does, including slider
307
+ changes made on pixellab.ai, so pixelkiln reads it back from there after
308
+ each export.
309
+ - A recipe stores only where the sprite stands (`subject: {x, y, align}`),
310
+ not its pixels, so re-rendering an effect built around a sprite needs the
311
+ sprite sent again.
312
+
313
+ Wired up as `pixelkiln vfx` for loose files ([the CLI reference](./CLI.md#vfx))
314
+ and as the `vfx` manifest generator ([GENERATORS.md](./GENERATORS.md#vfx)),
315
+ which cuts the exported sheet into an ordered frame set. The workbench
316
+ answers "the pixel tools are busy. retry in a few seconds." while another
317
+ render on the account is running; pixelkiln retries that with backoff.
318
+
319
+ ---
320
+
321
+ ## VFX artist (`POST /vfx`), measured once
322
+
323
+ PixelLab 0.4.130 added "Create VFX" to the REST API: an effects artist (the
324
+ website's VFX creator) that builds an effect from a plain-language
325
+ description, renders it, checks its own work, and saves it to My effects.
326
+ The request and price table come from the OpenAPI document.
327
+
328
+ **Measured once** (October 2026, Tier 3 account): `pixelkiln vfx
329
+ --description "a small orange fireball flying right, flickering flames,
330
+ short looping trail" --artist lite`, no sprite. The submit quoted 1
331
+ generation and the finished job's `usage` billed exactly 1 (balance 5104
332
+ to 5103).
333
+ The job finished in about 20 s. `files` held the GIF, the sheet, and the
334
+ atlas. The result was a 163×26 canvas, 8 frames at 12 fps, looping, anchor
335
+ `origin` at (80, 13), parts `fireball` and `trail`. The sheet is 489×78, a
336
+ 3×3 grid holding 8 frames, and the atlas is an Aseprite-style `frames` list
337
+ with per-frame `frame` rects and 83 ms durations. pixelkiln's slicer cut 8
338
+ correct frames from it. A sprite, the pro artist, a change, and the game
339
+ pack remain unmeasured.
340
+
341
+ **Request** (`VfxRequest`): `description` (required, up to 8000 characters;
342
+ with `effect_id`, what to change), `sprite` (optional base64 PNG or GIF, up
343
+ to 512×512 per frame; an animation is a GIF or a horizontal strip of
344
+ `sprite_frames`, 1–32), `sprite_fps` (1–60), `artist` (`lite`, the default,
345
+ or `pro`), `effect_id` (change a saved effect in place; it keeps its own
346
+ sprite), `conversation` (from a previous result, with its `effect_id`:
347
+ priced as a change), `name` (up to 80 characters), and `game_pack`.
348
+
349
+ **Response.** A 202 with `background_job_id`, `status`, `quoted_generations`,
350
+ and `usage`. Poll `GET /background-jobs/{id}` every 5–10 s (PixelLab says
351
+ 30–90 s usually). A completed job's `last_response` has `files` (no-auth
352
+ links: `<name>.gif`, `<name>.png` sheet, `<name>.json` with frames,
353
+ durations, `pivot`, and `meta.anchor`, `<name>-game.zip` with `game_pack`,
354
+ `<name>-normals.png` when a light made the sprite's normal map), `effect_id`,
355
+ `name`, `edit_url`, `message`, `canvas` `{w, h}`, `frames`, `fps`, `loop`,
356
+ `anchor` `{kind, x, y}`, `parts`, and `conversation`. The shape of `files`
357
+ (a list or a map) is not documented; pixelkiln reads a URL list, a list of
358
+ `{name, url}` records, or a name-to-URL map, and fails naming the keys it got
359
+ otherwise.
360
+
361
+ **Errors.** 400 is a request to change (the detail says what), 402 means
362
+ insufficient credits, 429 too many concurrent jobs, and 503 the renderer is
363
+ busy. The shared HTTP policy retries 429 and 503 with backoff.
364
+
365
+ **Cost** (generations), quoted at submit and charged on delivery: the quote,
366
+ or more when the effect took much longer (the overrun past 25% of the
367
+ quote), never more than twice the quote.
368
+
369
+ | | lite | pro |
370
+ |---|---|---|
371
+ | new effect, no sprite or up to 128 px | 1 | 6 (8 with a sprite) |
372
+ | change (with `conversation`) | 1 | 4 |
373
+ | sprite up to 256 px | 3 (change 2) | 14 (change 5) |
374
+ | sprite up to 512 px | 5 (change 2) | 28 (change 8) |
375
+
376
+ An animated sprite adds "a little per frame", unpriced; pixelkiln sends
377
+ still PNG sprites only. Plans and `--budget` count twice the quote, the most
378
+ a job can bill, and the lock records `quotedGenerations` beside the bill read
379
+ from the finished job's `usage`.
380
+
381
+ Wired up as `pixelkiln vfx --description` ([CLI](./CLI.md#described-effects-paid))
382
+ and as a `vfx` block's `description` ([GENERATORS.md](./GENERATORS.md#described-effects-the-paid-vfx-artist)).
383
+
384
+ ---
385
+
386
+ ## Normal map (`POST /normal-map`), measured once
387
+
388
+ PixelLab 0.4.130. The body is `image` (base64 PNG, 8 to 512px per frame
389
+ side; an animation is a horizontal strip), `frames` (1 to 32), and
390
+ `convention` (`opengl`, green up, the default; or `directx`). It returns a
391
+ background job, and the finished job's `last_response.image` is the map as
392
+ a PNG the input's size. PixelLab documents 0.5 generations, charged on
393
+ delivery.
394
+
395
+ **Measured once** (October 2026): `pixelkiln normal-map --frames 8` on a
396
+ 1304×26 strip of eight 163×26 effect frames billed exactly 0.5 generations
397
+ (balance 5103 to 5102.5) and returned a 1304×26 RGBA strip. Its alpha
398
+ matched the source's pixel for pixel, so the strip cuts back into the same
399
+ 8 frames. Wired up as the `normal-map` revision mode and `pixelkiln
400
+ normal-map` ([REVISIONS.md](./REVISIONS.md#normal-maps)).
263
401
 
264
402
  ---
265
403
 
266
- ## Tilesets: schema only, not yet measured
404
+ ## Tilesets: mostly schema, one sidescroller measured
267
405
 
268
406
  Called out separately because these are the most capable endpoints for level art
269
- and they are relevant to the disc-golf game. **Costs below are unmeasured.**
407
+ and they are relevant to the disc-golf game. **Top-down costs are unmeasured**:
408
+ pixelkiln plans a top-down set at PixelLab's own quote (Pro 15 for tiles up to
409
+ 32px, 25 at 64px; standard 4, the top of its quoted 1 to 4). The sidescroller
410
+ cost below is measured.
270
411
 
271
412
  `POST /tilesets` (top-down) and `/tilesets-sidescroller` are the current
272
413
  versions; `/create-tileset*` are the older aliases with identical schemas.
273
- The top-down family is wrapped by pixelkiln's `terrain` generator
274
- (skills/pixelkiln/references/pixellab.md); `/create-tileset-sidescroller` and
275
- the reference-image/`color_image` knobs below are not.
414
+ Both are wrapped by pixelkiln's `terrain` generator
415
+ (skills/pixelkiln/references/pixellab.md), the sidescroller one with
416
+ `terrainOrientation: "sidescroller"`; the reference-image/`color_image`
417
+ knobs below are not.
276
418
 
277
419
  They take `lower_description` + `upper_description` (+ optional
278
420
  `transition_description`). You describe two terrains and the transition between
@@ -282,8 +424,43 @@ them, and get a tileset that blends them. Distinctively, they accept **both**
282
424
  family that combines palette forcing with style anchoring.
283
425
 
284
426
  Useful knobs: `tile_size`, `tileset_adherence` / `tileset_adherence_freedom`
285
- (how strictly tiles must fit together), `raggedness` and `slope_size` for edge
286
- character, plus the usual `outline` / `shading` / `detail`.
427
+ (how strictly tiles must fit together), and the usual `outline` / `shading` /
428
+ `detail`. Pro's border controls are `raggedness`, `roundness`,
429
+ `boundary_amplitude` with `boundary_seed`, `step_slope`, and
430
+ `connect_diagonals`; `spread_x` and `slope_size` are documented as "No
431
+ effect" now and kept only so existing calls work.
432
+
433
+ `lower_base_tile_id` / `upper_base_tile_id` connect a new set to another
434
+ set's terrain. `GET /tilesets/{id}` (observed live) has a top-level
435
+ `metadata.terrain_ids` of `{ "lower": "<uuid>", "upper": "<uuid>" }`: those
436
+ are the base tile ids, so set A's `upper` id sent as set B's
437
+ `lower_base_tile_id` makes the two share that terrain. pixelkiln records them
438
+ as `terrainIds` when it polls a set.
439
+
440
+ ### Sidescroller, measured
441
+
442
+ One `POST /create-tileset-sidescroller` call, 16px tiles,
443
+ `transition_size: 0.25`, `lower_description` "mossy stone bricks texture",
444
+ `transition_description` "green grass texture", billed **exactly 3
445
+ generations** (balance 5107 to 5104). PixelLab's MCP tool quotes 2 or 3.
446
+
447
+ - The body has no `mode`, `view`, `upper_description`, or shape controls.
448
+ `tile_size` is 16 or 32 only, `transition_size` one of 0, 0.25, 0.5, 1.
449
+ `lower_base_tile_id` is the only connection field.
450
+ - Submit answers `{ tileset_id, background_job_id, status: "processing" }`,
451
+ with `usage: null`.
452
+ - `GET /tilesets/{id}` (and `/tilesets-sidescroller/{id}`) returns the
453
+ top-down shape: 16 tiles named `wang_0` to `wang_15`, each with `corners`
454
+ and `pattern_4x4`, images base64 inline. But `tileset.terrain_types` is
455
+ `["central", "background"]` while every corner still says `"lower"` (the
456
+ platform) or `"upper"` (empty, transparent air; the all-upper tile is
457
+ blank). pixelkiln records `["lower", "upper"]` so its corner masks work.
458
+ - `metadata.terrain_ids` has `lower` only, and `metadata.view` is `"side"`.
459
+ `metadata.transition_size` came back `0.0` although 0.25 was sent.
460
+ - `usage` is null on the GET, so the billed amount comes from
461
+ `GET /background-jobs/{background_job_id}` (`usage.generations: 3.0`).
462
+
463
+ A trimmed copy of that response is `test/fixtures/tileset/sidescroller-get.json`.
287
464
 
288
465
  `create-tiles-pro` is a different shape, a single `description` plus
289
466
  `style_images`, `tile_view`, `building_*` fields for structures.
@@ -9,10 +9,11 @@ account; [ENDPOINTS.md](./ENDPOINTS.md) contains the detailed experiments.
9
9
  | You need | Generator | Measured cost | Candidates/output |
10
10
  |---|---|---:|---|
11
11
  | Standalone prop or icon with arbitrary dimensions | `map` (default) | 1 generation | 1 |
12
- | Exact fixed palette | `pixflux` | 1 generation | 1 inline image |
12
+ | Exact fixed palette | `pixflux`, or `imagePro` for a larger or non-square canvas | 1 generation (`imagePro`: 10–25) | 1 inline image (`imagePro`: 1–64 by size) |
13
13
  | Candidate variety, richer rendering, future rotation/animation | `1dir` | 10–25 generations | 4–64 by size |
14
14
  | Ground tiles or connectable structural sets | `tiles` | 10–25 generations | variations or complete set |
15
- | Two-terrain Wang tileset for elevation (grass to water, floor to cliff) | `terrain` | 10–25 generations (unmeasured) | 16 or 25 tiles |
15
+ | Two-terrain Wang tileset for elevation (grass to water, floor to cliff), or a sidescroller platform set | `terrain` | Pro 15, or 25 at 64px; standard plans at 4 (PixelLab's quotes); sidescroller 3 (measured) | 16 or 25 tiles |
16
+ | Platform tileset for a sidescroller (stone body, grass top) | `terrain`, `terrainOrientation: "sidescroller"` | 3 generations | 16 tiles |
16
17
  | A single elevation tile — a raised mesa, a cliff block | `isometricTile` | 1 generation | 1 |
17
18
  | Larger or non-square scene or background, up to 792 wide and 688 tall | `imagePro` | 10–25 generations by canvas | 1–64 by size |
18
19
  | Controlled pose/expression sequence in ComfyUI | `frames` | 0 `free` provider units | one atomic ordered set |
@@ -22,13 +23,16 @@ account; [ENDPOINTS.md](./ENDPOINTS.md) contains the detailed experiments.
22
23
  | UI chrome — panels, buttons, health bars, toolbars | `uiAsset` | 20 generations (measured once, at 256x192) | 1 composited image |
23
24
  | One UI element from a description, 16px and up, optionally guided by a concept image | `uiElement` | 10–25 generations (unmeasured) | 1 image |
24
25
  | A styled still on the Pro Flash model, or the south sprite a Pro Flash character will rotate | `imageProFlash` | 5–9 generations (PixelLab's provisional quote) | 1 image |
26
+ | An animated effect (spell, hit, explosion, aura), on its own or around a sprite | `vfx` | free from presets and parts; 1–28 when described to PixelLab's artist (lite measured at 1; plans at twice the quote) | one ordered frame set |
25
27
 
26
- Start with `map` unless a required capability points elsewhere. Forty `map`
27
- re-rolls cost the same as one 64×64 `1dir` call.
28
+ Start with `map` unless a required capability points elsewhere. Twenty-five
29
+ `map` re-rolls cost the same as one 64×64 `1dir` call.
28
30
 
29
31
  `terrain` and `imagePro` have no section below: their fields and limits are in
30
- the [PixelLab skill reference](../skills/pixelkiln/references/pixellab.md), and
31
- a `terrain` set's export shape is in [Tiles](./TILES.md). Retro Diffusion
32
+ the [PixelLab skill reference](../skills/pixelkiln/references/pixellab.md),
33
+ including Pro's border controls, sidescroller sets, and connecting sets that
34
+ share a terrain (an asset's `connect`), and a `terrain` set's export shape is
35
+ in [Tiles](./TILES.md). Retro Diffusion
32
36
  `animation` styles are in [Set up Retro Diffusion](./RETRO_DIFFUSION.md).
33
37
 
34
38
  ## `map`
@@ -535,6 +539,154 @@ billed yet. The plan prices it on the same 10/15/25 canvas tiers as
535
539
  job's shape is read defensively (`pollUiElement` names the keys it got if
536
540
  the shape is not the documented `images` list).
537
541
 
542
+ ## `vfx`
543
+
544
+ `vfx` renders animated pixel-art effects with PixelLab's VFX workbench:
545
+ spells, hits, explosions, auras, shields, status icons, weather. The workbench
546
+ is a renderer, not a model. An effect is a recipe of layers, most of them
547
+ dropped in as tuned parts, and it renders the same pixels every time. It is
548
+ free on a PixelLab subscription, and PixelLab offers it only over MCP, which
549
+ pixelkiln calls with the same `PIXELLAB_API_KEY`. An effect can also be
550
+ described in words for PixelLab's paid VFX artist; see
551
+ [Described effects](#described-effects-the-paid-vfx-artist).
552
+
553
+ ```jsonc
554
+ {
555
+ "styles": {
556
+ "fx": { "generator": "vfx", "outDir": "art/fx" }
557
+ },
558
+ "assets": {
559
+ "robot.zap": {
560
+ "prompt": "lightning on the robot",
561
+ "vfx": { "preset": "lightning_strike", "sprite": "art/chars/robot-south.png" }
562
+ },
563
+ "boom": {
564
+ "vfx": { "preset": "explosion", "parts": [{ "name": "scorch_mark" }], "fps": 12 }
565
+ },
566
+ "zap.tuned": {
567
+ "vfx": { "recipe": "fx/zap.vfx.json" }
568
+ }
569
+ }
570
+ }
571
+ ```
572
+
573
+ - Start from one `preset` (`pixelkiln vfx presets` lists them) or one
574
+ `recipe`, and add `parts` (`pixelkiln vfx parts`) on top. `parts` alone also
575
+ start an effect. Each part is `{ "name": … }` plus any of the workbench's
576
+ own options, sent as given: `size`, `colour`, `amount`, `speed`, `start`,
577
+ `end`, `anchor`, `at`, `offset`, `travel`, `role`, `params`.
578
+ - `recipe` is a bare recipe JSON or a `.vfx.json` record written by
579
+ `pixelkiln vfx`. That is how an effect tuned by hand, in the workbench or
580
+ with the sliders on pixellab.ai, enters a manifest: export it with
581
+ `pixelkiln vfx --effect <id> --out fx/zap`, then point `recipe` at the
582
+ record.
583
+ - `sprite` builds the effect around a character: a manifest-relative PNG or
584
+ JPEG, or a `character:<id>:<direction>`, `object:<id>`, or `gallery:<id>`
585
+ reference. A path may be another asset's output. Until that file exists
586
+ the effect is blocked, and `gen` renders it in a later wave once it does.
587
+ The exported frames include the sprite.
588
+ - `fps` overrides the preset's or recipe's own rate. `name` is the effect's
589
+ name in your PixelLab My effects; it defaults to `pixelkiln:<style>/<asset>`.
590
+ `prompt` is optional and only a label.
591
+ - An asset that lists no `styles` joins every style of its own kind: a `vfx`
592
+ style takes the assets with a `vfx` block, and every other style the
593
+ assets without one. A mirror or revision of an effect names its style.
594
+
595
+ The workbench builds an effect from tuned parts. An effect that is a drawn
596
+ object or a character's own motion, like a sword turning into lightning or a
597
+ mage casting a fireball, is a job for generation instead: an `imagePro`
598
+ sheet of candidates, then an `animate-pixminimax` revision with an optional
599
+ `lastFrame`. PixelLab's
600
+ [VFX tutorial](https://www.youtube.com/watch?v=t8FIMv-8QpI) shows that
601
+ workflow, and [Set up PixelLab](./PIXELLAB.md#animated-effects) maps it onto
602
+ PixelKiln.
603
+
604
+ **Outputs.** The exported spritesheet is cut into frames,
605
+ `<asset>-frame-00.png` onward, recorded as an ordered frame set with the
606
+ effect's fps and whether it loops. `pack` lays them out like any loop, and
607
+ `pack --format godot` writes a one-shot effect with `loop: false`. The
608
+ gallery plays them, shows the rendered size, and links each generation to
609
+ its effect on pixellab.ai.
610
+
611
+ **Identity.** An effect goes stale when its preset, parts, fps, recipe
612
+ content, or sprite content changes. It does not go stale for a renamed or
613
+ moved file, a new `name` or `prompt`, or a style's prompt, size, or palette
614
+ settings, none of which reach the workbench. Plan cannot see slider changes
615
+ made on pixellab.ai or changes to PixelLab's own presets.
616
+
617
+ **Regenerating.** A regeneration exports over the effect the asset rendered
618
+ last time, so My effects keeps one entry per asset. If that effect was
619
+ changed on pixellab.ai since, or deleted, a new effect is saved instead and
620
+ the edited one is left alone; the lock records why.
621
+
622
+ ### Described effects: the paid VFX artist
623
+
624
+ A `description` in place of a preset, recipe, or parts asks PixelLab's VFX
625
+ artist (`POST /vfx`, added in 0.4.130) to build the effect. The artist is the
626
+ same one behind the website's VFX creator: it picks and tunes parts from your
627
+ words, renders the effect, checks its own work, and saves it to My effects.
628
+
629
+ ```jsonc
630
+ "zap.described": {
631
+ "vfx": {
632
+ "description": "a blue lightning strike that hits the ground and leaves sparks",
633
+ "artist": "pro",
634
+ "sprite": "art/chars/robot-south.png"
635
+ }
636
+ }
637
+ ```
638
+
639
+ Use the free workbench when a preset or a few parts get you there, or when
640
+ you want to tune the effect by hand; it renders the same pixels every time.
641
+ Use the artist when you would rather describe the effect than assemble it.
642
+ It costs generations, and the same description draws a different effect
643
+ each time.
644
+
645
+ - `description` is the effect in plain words: what it is, its colours, how
646
+ it moves, how long. It cannot be combined with `preset`, `recipe`,
647
+ `parts`, or `fps`; the artist picks those itself.
648
+ - `artist` is `lite` (the default, quick and cheap) or `pro` (the stronger
649
+ artist, for detailed effects).
650
+ - `sprite` must be a local PNG up to 512×512, another asset's output
651
+ included. The endpoint takes the sprite's pixels, so `character:`,
652
+ `object:`, and `gallery:` references are refused; point at the
653
+ character's own output file instead. JPEG is refused too.
654
+ - `name` is capped at 80 characters, and the default is cut to fit.
655
+
656
+ **Cost.** PixelLab quotes each job when it is submitted and bills on
657
+ delivery: the quote, or more when the effect ran long, never more than twice
658
+ the quote. The quote comes from this table (generations):
659
+
660
+ | | lite | pro |
661
+ |---|---|---|
662
+ | no sprite, or a sprite up to 128 px | 1 | 6 (8 with a sprite) |
663
+ | sprite up to 256 px | 3 | 14 |
664
+ | sprite up to 512 px | 5 | 28 |
665
+
666
+ `plan` and `--budget` count **twice the quote**, the most a job can bill, so
667
+ a budget is never overrun. A sprite that is another asset's output not drawn
668
+ yet is priced at the 512 px tier until it exists. The lock records PixelLab's
669
+ own quote (`quotedGenerations`) and the bill read from the finished job.
670
+ These prices are PixelLab's published table; no artist bill has been
671
+ measured here yet.
672
+
673
+ **Outputs and identity.** The artist's spritesheet is cut into the same
674
+ ordered frame set as the workbench's, with its fps and loop flag. The lock
675
+ records the effect id, its edit link, the artist's message, the anchor, the
676
+ parts it used, and the file names. The asset goes stale when its
677
+ `description`, `artist`, or sprite content changes, and not for `name` or
678
+ `prompt`. Adding these fields left every workbench effect's identity as it
679
+ was.
680
+
681
+ **Regenerating.** Every regeneration is a new effect, billed again. The
682
+ manifest's description is the whole effect, not a change to the last one,
683
+ so it is never sent as one. The previous effect stays in My effects, and the
684
+ lock names it (`previousEffect`). To change one saved effect in place at the
685
+ change price, use `pixelkiln vfx --description "<what to change>" --effect
686
+ <id>` ([CLI](./CLI.md#vfx)). To turn a described effect into a free,
687
+ repeatable recipe, export it with `pixelkiln vfx --effect <id> --out
688
+ fx/zap` and point a workbench `recipe` at the record.
689
+
538
690
  ## Style variants
539
691
 
540
692
  Styles are namespaces. Add another style to re-derive the same asset ids into a