Pixellab
Pixel-art asset generation for game projects, MCP-first via the connected pixellab MCP server (mcp__pixellab__* tools). Triggers on: "generate pixel art", "make a pixel sprite/tile/character", "animate this sprite", or any pixel-art asset request for app/games/**. Falls back to scripts/pixellab.mjs only for animated WebP assembly or as a REST emergency path.From its SKILL.md
npx -y skills add freema/ai-skills --skill pixellabAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
15.4 KB, as published. Nobody here has run it
PixelLab Skills (MCP-first)
Pixel-art asset generation for app/games/**. The PixelLab MCP server is the default
path — it is connected this session as mcp__pixellab__* tools, lands every asset in the
PixelLab dashboard (reusable, style-chainable), and bills against our subscription. Use the
local scripts/pixellab.mjs only for the one thing the MCP can't do (assemble an animated
WebP) and as an emergency REST fallback.
Official docs / disambiguation: mcp__pixellab__agent_help (a docs-search agent — ask it
≤500-char questions). MCP tool reference: https://api.pixellab.ai/mcp/docs · REST reference:
https://api.pixellab.ai/v2/llms.txt (+ full param/cost detail in …/v2/openapi.json).
Golden rules
- MCP-first. Generate through
mcp__pixellab__*. Reach forscripts/pixellab.mjsonly forwebpassembly or if the MCP server is unreachable. - Write BIG, exact descriptions. PixelLab rewards long, fully-specified prompts: subject, view angle, palette, lighting, material, silhouette, and an explicit list of what to EXCLUDE. Vague prompts waste generations. (See "Writing prompts".)
- Style-lock related assets with
create_object_state/create_character_stateon a completed parent — never cold-generate siblings (loses palette/identity). See [[feedback-pixellab-card-state-parent]], [[feedback-claimer-sprite-refs]]. - Never save binary with the Write tool — it corrupts PNGs to zero-filled garbage.
curl -Lthe rotation URL, thenfile-verify. - Bump the filename on any asset replacement (
-v3→-v4) —public/assets/**is immutable-cached 1y + 30d nginx. See [[feedback-nginx-static-cache]]. - PixelLab only — no PixelForge fallback for game sprites. See [[feedback-pixellab-only-for-games]].
⚠️ Tool-name reality check
There is NO create_object and NO vary_object tool — those names (in older memory) were
never real. The actual primitives on the connected server are:
| You want… | Real MCP tool |
|---|---|
| One static sprite / prop / icon | create_1_direction_object |
| Same object from 8 angles (rotations) | create_8_direction_object |
| A variant in the same style as an object | create_object_state (source object_id + edit_description) |
| Throwaway object (auto-deletes 8h) + inpaint | create_map_object |
| Rigged character (4/8 dir, walk cycles) | create_character → create_character_state → animate_character |
| Motion on an existing object | animate_object (mode v3 = cheap default) |
| Terrain autotiling | create_topdown_tileset / create_sidescroller_tileset |
| Tile variants / isometric | create_tiles_pro / create_isometric_tile |
| Poll any of the above | get_object / get_character / get_*_tileset |
Pick from a review candidate grid | select_object_frames / dismiss_review |
| Balance | get_balance |
Canonical flow: create → poll → download (verified)
Generation is asynchronous. The create call returns an id immediately; you poll until
completed, then download the PNG from a public URL.
1. create_1_direction_object({ description, size }) → returns object_id
2. get_object({ object_id, include_preview: false }) # poll every ~10–30s
status: processing → progress + ETA, keep polling
status: review → candidate grid (see "Review gate") — select or dismiss
status: completed → has `rotations:` + `download:`
status: failed → error + retry hint
3. Download the PNG. `get_object` (completed) returns, exactly:
rotations:
<dir>: https://backblaze.pixellab.ai/file/pixellab-characters/objects/<grp>/<id>/rotations/<dir>.png
download: https://api.pixellab.ai/mcp/objects/<id>/download # zip archive of all frames
For a 1-direction object the rotation key is literally `unknown`.
4. curl -L "<rotation-url>" -o public/assets/games/<game>/<name>-v1.png # NO auth header needed
5. file public/assets/games/<game>/<name>-v1.png # MUST say "PNG image data, WxH, 8-bit/color RGBA"
Download facts (verified live): rotation URLs are public Backblaze links — a plain
curl -L with no Authorization header returns the real PNG (HTTP 200). The MCP never
returns raw bytes and never writes files; it only hands back URLs (+ an optional inline
preview when include_preview is true — for eyeballing, not for saving). Poll budgets:
1-dir/tiles ~15–90s, 8-dir/characters ~2–4 min, tilesets ~100s.
Review-status gate (don't lose your asset)
create_1_direction_object with size ≤ 170 returns multiple candidates in review
status (≤42→64 candidates, ≤85→16, ≤170→4). get_object shows them inline; you must then
call select_object_frames({ object_id, indices: [n] }) to promote the good one(s) into their
own completed objects, or dismiss_review to discard. Leave it and the asset never finalizes.
Use size ≥ 171 to get a single candidate with no review step.
Style consistency: object-states & style-refs
Build a family of related sprites by editing a completed parent, not by re-prompting:
create_1_direction_object({ description: "<the first/base sprite>", size: 48 }) → parentId
create_object_state({ object_id: parentId, edit_description: "make it a green shell instead" })
create_object_state({ object_id: parentId, edit_description: "make it a lightning bolt instead" })
All variants share palette/outline/identity via an automatic group_id. This is the
proven pattern for the mau-mau card faces (parent 06f843eb-abc3-4b2e-a7ee-c2974acbebb1 —
create_object_state ONLY, never a fresh create_*) and the claimer directional sprites.
For style-matching across a totally new object, pass style_images to create_1_direction_object
(largest style image's size sets output size — don't also pass size).
Writing prompts (the single biggest quality lever)
PixelLab interprets a long, exact description far better than a terse one. Always specify: subject + view + palette + lighting/shading + material/texture + silhouette + EXCLUSIONS.
✅ Good: "Single top-down pixel-art banana-peel power-up icon, 48×48, lineless flat shading, transparent background. Glossy yellow curved peel lying flat as if dropped on a track, small brown bruise tip, soft inner neon-yellow rim glow, dark drop-shadow blob beneath. Centered, no text, no border, no frame."
❌ Bad: "a banana item" — model guesses size, view, palette, adds a background/border.
outline: "lineless"almost always (user hates borders).- For opaque scenes add "edge to edge, full bleed, no border, no frame, no padding" or you get a white-canvas vignette. For "no characters" say so explicitly — models populate scenes by default.
detail: "medium detail",shading: "medium shading"are good defaults. Useseedfor reproducibility.
Cost model (dual bucket — the old "16 credits/call" rule is WRONG)
get_balance shows two separate buckets:
- Subscription generations — the primary bucket (Tier 1 = 2000 per billing period). Each call decrements this first.
- USD credits — a fallback only, used when generations are exhausted.
Cost is per call, by endpoint + size — NOT per image. One v2 call can return a grid of up to 64 images for the same generation cost. So:
- Standard object/character = 1 generation. v3 character = 2–9.
create_1_direction_object/create_8_direction_objectuse Pro Tools = ~20–40 generations per call by size.- The only real cost cliff is Pro-mode animation: 20–40 generations PER DIRECTION (a full
8-dir Pro animation = 160–320 = ~6–12 of those empties the monthly allowance). Prefer
mode: "v3"(the cheap default, often higher quality). For Proanimate_*, call withoutconfirm_costfirst to get the quote, surface it, then re-call withconfirm_cost: true. enhance_promptsilently adds 0.05 generations.
Check get_balance before a big batch; budget by calls, not by samples.
File-saving rules
- Never use the Write tool for image data (corrupts binary → zero-filled file).
- Download via
curl -L "<rotations.<dir>>" -o <path>(no auth header). Thedownload:URL is a zip archive of all frames — use it only when you want every rotation/frame at once. - Always
file <path>— must reportPNG image data, …, RGBA. If it saysdata/JPEG/RIFF, it's broken; regenerate, don't patch in place. - Optimize after:
pngquant --force --quality=60-85 --ext .png <path>(60–85% is indistinguishable for pixel art; a 256×384 bg drops ~130KB → ~50KB). - Replacing an existing asset? Bump the filename suffix and update the loader key (nginx cache).
Animation & animated thumbnails
- In-engine motion:
animate_object({ object_id, animation_description, mode: "v3" })(1 gen/dir,frame_count4–16, default 8 → stores 9 incl. a ref frame). Pollget_object; completed response listsanimations:with downloadable frame URLs (Backblaze, same as rotations). - Animated thumbnails / menu loops: download the frames, then assemble locally with
scripts/pixellab.mjs webp --in-dir <frames> --out <x>.webp --duration 120. The MCP/REST cannot assemble a WebP — this is the one irreplaceable local step. See [[feedback-pixellab-animate-webp]]. - Pixel-exact thumbnails (compose the game's REAL sprites with overlay sparkle/pulse, then WebP):
prefer this when identity must be preserved — generative
animate-with-text-v3reinterprets the concept and will NOT keep your exact pixels. (The reason is pixel-identity, not cost.) Full PIL recipe in [[feedback-pixellab-animate-webp]] / the api-v2-extra reference.
scripts/pixellab.mjs — fallback + WebP assembler (NOT broken)
The audit confirmed its commands are valid REST calls; it is superseded for static work, not
broken. Keep it. Reads PIXELLAB_API_KEY from .env (already set).
| Command | Status |
|---|---|
webp | LOAD-BEARING — local img2webp (libwebp); no MCP/REST equivalent. |
sprite / background / style | Valid generate-image-v2/generate-with-style-v2 fallback. Default to MCP create_1_direction_object / create_object_state instead (dashboard + style-lock). |
animate | Generative (breaks pixel identity) — prefer MCP animate_object v3. |
balance | Redundant with MCP get_balance. |
Hard host prerequisite for webp: img2webp from libwebp (brew install webp). One genuine use
where the script wins: a wide panoramic banner (e.g. a skybox strip) — MCP object tools pad
toward square, so pixellab.mjs sprite with an explicit wide --size is the right tool there.
⚠️ But generate-image-v2 caps aspect at ~688×384 (16:9) — extreme strips (e.g. 1024×192) exceed it;
generate a seamless tile within the cap and let the engine TileSprite repeat it.
Tilesets — Wang (top-down) PREFERRED for terrain
create_topdown_tileset → 16 Wang tiles (23 if transition_size: 1.0), 32×32, outline: "lineless".
lower_description = ground, upper_description = elevated, transition_description = edge blend.
~100s. Chain seamless multi-terrain via the returned base tile IDs.
Download: curl -L --fail (302 redirect → 0 bytes without -L).
Wang index + Phaser integration (vertex-terrain algorithm, frame lookup, RenderTexture layering) — unchanged and correct; see the detailed recipe in the api-v2-extra reference and below.
Wang index = NW×8 + NE×4 + SW×2 + SE×1 (upper=1, lower=0)
Frame = (bbox.y/32)*4 + (bbox.x/32)
Vertex(vr,vc) = 0 if ANY surrounding cell is target terrain, else 1
- Sidescroller platform tiles:
create_sidescroller_tileset— see sidescroller-tilesets.md. - Tiles Pro / isometric:
create_tiles_pro(number each variant:"1). grass 2). dirt …",nmust match) /create_isometric_tile(tile_shapethin/thick/block). Small pickups look BAD as tiles — use Graphics primitives for tiny in-game objects; reserve tiles for 32×32+ decor. - Corner-tile orientation is often mislabeled — verify at 10× zoom, or just use Wang.
Characters & enemies
- Humanoid / quadruped:
create_character(body_type,mode: standard|v3|pro,n_directions4|8). Standard = 1 gen; v3 = 2–9 (always 8-dir, top quality); pro = 20–40. Canvas is ~40% larger than the requested size (leaves animation room) — slice sheets accordingly. - Variants (armor color, sitting, damaged):
create_character_state(consistent across all rotations). - Animate:
animate_character—template_animation_id(walk/run/idle… 1 gen/dir) oraction_description(custom, v3). South = default facing; West = East withsetFlipX(true). - Blobs / slimes / non-humanoid: NOT
create_character(forces legs). Usecreate_1_direction_object/create_map_objectper pose, describe "no legs, no arms, blob body".
Gotchas
- Base64 image inputs use
{ "type": "base64", "base64": "<png-bytes-b64>", "format": "png" }— base64-encoded PNG, not raw RGBA, not a data URL. - Size limits vary per endpoint: generic min 16×16; animation/rotation max 256×256; generate-image-v2/style/ui square ≤512 (16:9 ≤688×384); animate-with-text v1 locked to 64×64; tiles-pro 16–128. Let the API's 400 error tell you the real max for your aspect ratio.
- One v2 image call returns a GRID (up to 64 tiny images), not a single file — parse accordingly.
no_backgrounddefaults FALSE on legacycreate-image-*, TRUE on the v2 Pro endpoints.- Raw-RGBA vs PNG: some REST responses are base64 raw-RGBA, not PNG;
pixellab.mjsauto-wraps these. MCP rotation URLs are always real PNGs (verified). Alwaysfile-check. - Destructive deletes (
delete_object/_character/_animation) need aconfirm=false-then-truetwo-step; tileset/tile deletes take just the id. create_map_objectauto-deletes after 8h — download immediately.
General
get_balanceis the live source of truth for budget (not the RESTbalancecommand).- Backblaze rotation/B2 URLs are permanent and public — safe to reference while iterating.
- Always check status with
get_*before downloading; jobs are auto-cleaned after completion (404).