com um clique
pixellab
// Generate pixel-art tilesets and sprites via PixelLab MCP. Use when creating sidescroller platform tiles, top-down Wang tilesets, or other pixel-art assets through the PixelLab MCP server.
// Generate pixel-art tilesets and sprites via PixelLab MCP. Use when creating sidescroller platform tiles, top-down Wang tilesets, or other pixel-art assets through the PixelLab MCP server.
Build 2D browser games with Phaser 3 (JS/TS): scenes, sprites, physics (Arcade/Matter), tilemaps (Tiled), animations, input. Trigger: 'Phaser scene', 'Arcade physics', 'tilemap', 'Phaser 3 game'.
Write player-focused, SEO-optimized blog posts for PixelDen. Use for any new article in content/blog/. Hard rule: NO technical/devlog content (no tech stack, code, internals, file paths, framework names). Audience is players, not developers.
Generate game assets via Google Gemini API and process them into final sprite files. Use when creating new game sprites, generating concept art, or running the asset pipeline (Gemini image generation, sprite sheets, Imagen).
Use when the user wants to clean halo fragments or imperfect alpha from native-resolution pixel art sprites (PixelLab, Aseprite, or handmade). Invokes the Node/TypeScript CLI at scripts/pixelart-cleanup. Do NOT trigger for upscaled AI pixel art where one logical pixel spans many real pixels — that needs grid detection instead.
Post code review verdicts to Jira. Use after a code review is done and the user wants to sync the result to Jira. Triggers on: "post to Jira", "update the ticket", "sync review to Jira", or when a review is complete and Jira context is available (project key, issue number, or Atlassian MCP connected). Posts a concise verdict comment, flags blockers, and suggests workflow transitions.
Sandwich-structured code review for PRs, MRs, diffs, or pasted code. Triggers on: "review this", "check my PR/MR", "what do you think of this code", "is this OK to merge", or any paste of code/diff where the user wants feedback. Produces motivating reviews that clearly separate blockers from nitpicks. Always use this skill — even for quick reviews. The structure matters.
| name | pixellab |
| description | Generate pixel-art tilesets and sprites via PixelLab MCP. Use when creating sidescroller platform tiles, top-down Wang tilesets, or other pixel-art assets through the PixelLab MCP server. |
Read these BEFORE working on the relevant feature:
| When working on... | Read first |
|---|---|
| Sidescroller platform tiles (2D platformer) | sidescroller-tilesets.md |
| Top-down Wang tilesets (strategy/RPG maps) | See Wang section below |
create_sidescroller_tileset → 16 Wang tiles, 32×32, transparent bglower_description = material, transition_description = surface decorationtransition_size: 0.25 (light) or 0.5 (heavy surface)base_tile_id + high tileset_adherence (300–400) for matching setsoutline: "lineless"Generate: create_topdown_tileset
lower_description = ground terrain (e.g. "dark brown dirt path")upper_description = elevated terrain (e.g. "dark grey stone floor")transition_description = edge blend (e.g. "crumbling edge")tile_size: {width: 32, height: 32}outline: "lineless" (user preference!)transition_size: 0.25 (small transition) or 0.5 (larger)view: "high top-down" for flat tilesDownload: PNG (4×4 spritesheet, 128×128) + metadata JSON
curl --fail -o wang-tileset.png "https://api.pixellab.ai/mcp/tilesets/{id}/image"
curl --fail -o wang-tileset.json "https://api.pixellab.ai/mcp/tilesets/{id}/metadata"
Build frame lookup from JSON:
corners: {NW, NE, SW, SE} = "upper" | "lower"WANG_FRAME[wangIdx] = frameRender in Phaser:
// boot.ts
this.load.spritesheet("wang-tileset", url, {
frameWidth: 32,
frameHeight: 32,
});
// game.ts — vertex terrain algorithm
// Vertex (vr,vc) sits between cells (vr-1,vc-1), (vr-1,vc), (vr,vc-1), (vr,vc)
// Vertex = 0 (lower) if ANY surrounding cell is target terrain, else 1 (upper)
const vertex = (vr, vc) => {
return isPath(vr - 1, vc - 1) ||
isPath(vr - 1, vc) ||
isPath(vr, vc - 1) ||
isPath(vr, vc)
? 0
: 1;
};
// For each grid cell:
const nw = vertex(r, c);
const ne = vertex(r, c + 1);
const sw = vertex(r + 1, c);
const se = vertex(r + 1, c + 1);
const wangIdx = nw * 8 + ne * 4 + sw * 2 + se;
this.add.image(x, y, "wang-tileset", WANG_FRAME[wangIdx]);
base_tile_ids.upper and .lowerlower_base_tile_id in next tileset for seamless multi-terrain:
lower_base_tile_id param may error — generate independently with matching descriptions insteadWANG_FRAME lookup for allwangIdx === 0 (all-lower = base shows through)RenderTexture to composite all layers into one texture (performance)curl -L --fail (API returns 302 redirect, without -L you get 0 bytes)tile_type: "square_topdown", tile_view: "top-down", tile_size: 32outline: "lineless" — user hates borders"1). tile A 2). tile B 3). tile C"n_tiles must match description countcreate_characterbody_type: "humanoid" — bipedal (people, robots, knights)body_type: "quadruped" + template — 4-legged (bear, cat, dog, horse, lion)setFlipX(true) in Phaseranimate_character for walk/run/attack framescreate_map_objectcreate_character for blobs/slimes — humanoid template forces legs!create_map_objectscripts/pixellab.mjs (Node, replaces the old bash)The project ships scripts/pixellab.mjs — a zero-dependency Node CLI for the PixelLab REST API v2. The older scripts/pixellab.sh has been removed because it hid flags inside commands and mis-saved raw-RGBA responses as broken PNGs.
API key: PIXELLAB_API_KEY from .env (auto-loaded).
Full API reference: https://api.pixellab.ai/v2/llms.txt — read this first when adding new commands.
| Need | Use |
|---|---|
| Isolated item/sprite (transparent bg) | pixellab.mjs sprite |
| Scene / background (opaque pixels) | pixellab.mjs background |
| New sprite matching reference style | pixellab.mjs style |
| Animate a static frame | pixellab.mjs animate |
| Combine frames into animated WebP | pixellab.mjs webp |
| Check credit/generation balance | pixellab.mjs balance |
| Characters + per-direction animations | MCP: create_character + animate_character |
| Wang / sidescroller / topdown tilesets | MCP: create_*_tileset |
| Map objects (barrels, chests, items) | MCP: create_map_object |
./scripts/pixellab.mjs sprite \
--description "raw copper ore chunk, orange veins, side view" \
--size 64x64 \
--out public/assets/games/mygame/tier-02-copper.png
no_background: true — output has transparent pixels around the subject.792×688 but constrained by aspect ratio (see gotcha below)../scripts/pixellab.mjs background \
--description "dark mining cave interior, purple crystals, no people, no characters, edge to edge full bleed, no border, no frame, no padding" \
--size 256x384 \
--out public/assets/games/mygame/bg.png
no_background: false — keeps the scene opaque../scripts/pixellab.mjs style \
--ref public/assets/games/mygame/hero.png \
--description "goblin with a rusty dagger" \
--size 128x128 \
--out /tmp/goblin.png
style_images: [{image, width, height}] shape — the Node script reads PNG dimensions directly from the IHDR chunk../scripts/pixellab.mjs animate \
--input /tmp/hero-south.png \
--action "walking forward" \
--frames 8 \
--out-dir /tmp/hero-walk
animate-with-text-v3 is GENERATIVE — the API reinterprets your input as a concept. It does NOT preserve pixel-exact sprite identity. Characters/scenes may look subtly different in each frame. Great for character walk cycles, bad for "animate my exact 16-tile grid". For pixel-perfect animation of known sprites, composite programmatically with Python PIL (see "Programmatic animated thumbnails" below)../scripts/pixellab.mjs webp \
--in-dir /tmp/hero-walk \
--out public/assets/games/mygame/hero-walk.webp \
--duration 150
img2webp from libwebp (must be on PATH — installed via Homebrew alongside cwebp).--duration is milliseconds per frame. 120-150ms = smooth loop for thumbnails.The absolute max of generate-image-v2 is 792×688, but the API clamps based on your aspect ratio:
API 400: image_size must be between 16x16 and 424x632 for this aspect ratio. Got 480x688
Translation: for a tall portrait (~0.7 aspect) the actual max is 424×632. Let the API error message tell you the real limit — don't guess. Square images go up to ~500+, very-portrait/landscape ratios get squeezed.
Symptom: You call background with a scene prompt ("mining cave with crystals"), get a 128×192 PNG back, and every edge pixel is (254, 254, 254) — white.
Cause: Without explicit "edge to edge, no border, no padding" language, generate-image-v2 treats your scene as a centered subject and pads everything outside with a white canvas. A blurry vignette in the middle, white around it. no_background: false does NOT fix this — the white IS the background the model chose to draw.
Fixes:
"...edge to edge, full bleed composition, no white border, no frame, no padding, fills entire canvas seamlessly"256×384 or bigger usually fills the frame.python3 -c "from PIL import Image; img=Image.open('bg.png').convert('RGBA'); print(img.getpixel((0,0)))" — if the top-left pixel is near-white, regenerate.PixelLab sometimes returns the generated image as base64-encoded raw RGBA pixel bytes instead of a base64-encoded PNG. The old pixellab.sh blindly wrote those bytes to .png, creating files that file reports as "data" (not PNG) and nothing can render.
The Node script auto-detects this: it checks the first 8 bytes for the PNG signature (89 50 4e 47 0d 0a 1a 0a). If missing, it wraps the raw RGBA into a proper PNG via the built-in zlib encoder. You don't need to think about this — just always file <path> after a generation to confirm.
file public/assets/games/mygame/*.png
# → "PNG image data, WxH, 8-bit/color RGBA, non-interlaced"
If any file says "data" or "JPEG image data" or "RIFF" when you expected PNG, it's broken. Regenerate before moving on — don't ship broken assets and don't try to fix them in-place.
# Crunch PNGs — 60-85% quality is virtually indistinguishable for pixel art
pngquant --force --quality=60-85 --ext .png public/assets/games/mygame/*.png
A fresh bg.png at 256×384 can drop from ~130KB to ~50KB with no visible difference.
PixelLab MCP is a remote server — it returns base64/URL data but CANNOT write files to disk.
curl -L --fail on the download URL the MCP call returns — that already gives you a real PNG. No base64 re-decoding needed.file <path> — must show PNG image data, not data, JPEG, or RIFF# 1. Call MCP (create_map_object, create_character, etc.) → returns an object ID
# 2. Call the corresponding get_* MCP tool → returns a download URL
# 3. Download with curl:
curl -L --fail -o public/assets/games/mygame/tile.png \
"https://api.pixellab.ai/mcp/map-objects/{object_id}/download"
# 4. Verify:
file public/assets/games/mygame/tile.png
# → must say "PNG image data, ..."
In a previous incident, Claude used the Write tool to save PixelLab base64 data. Write only handles UTF-8 text — binary PNG bytes get corrupted to zero-filled buffers. Result: 145 broken files that looked like PNGs but contained only null bytes. Use curl for URLs, pixellab.mjs for API v2, and always file-verify.
Games can have animated thumbnails (shown on catalog page) and animated menu backgrounds (shown in Phaser menu scene). There are two distinct approaches depending on whether you need to preserve exact pixel identity.
Use this when the user wants "animate my thumbnail using the game's real sprites". animate-with-text-v3 reinterprets the input as a concept and will NOT preserve your pixel-exact tiles — it generates new gems/characters that look similar but are not your sprites.
Instead, composite a static grid from the actual tier PNGs, then generate frames in Python that add overlay effects (sparkles, pulse, drifting particles) while keeping the base sprites pixel-perfect. Then stitch into an animated WebP.
Workflow (used for ore-merge, ~140KB WebP, 12 frames):
# 1. Composite actual game sprites into a grid layout
from PIL import Image, ImageDraw, ImageEnhance
import math, random
base = Image.new('RGBA', (288, 288), (20, 16, 30, 255)) # dark card bg
draw = ImageDraw.Draw(base)
GRID, CELL, GAP, PAD = 4, 64, 4, 10
for row in range(GRID):
for col in range(GRID):
cx = PAD + col * (CELL + GAP)
cy = PAD + row * (CELL + GAP)
# Darker cell background so sprites pop
draw.rectangle([cx, cy, cx + CELL - 1, cy + CELL - 1], fill=(30, 27, 46, 255))
sprite = Image.open(f'public/assets/games/mygame/tier-{tier_idx:02d}.png').convert('RGBA')
base.alpha_composite(sprite, (cx, cy))
base.save('/tmp/composite.png')
# 2. Generate N animation frames with overlay effects
random.seed(42)
sparkles = [(random.randint(12, 276), random.randint(12, 276), random.choice([(255,215,50),(200,120,255)]), random.random()*math.pi*2) for _ in range(14)]
for i in range(12):
frame = base.copy()
t = i / 12
# Subtle scene-wide brightness pulse
frame = ImageEnhance.Brightness(frame).enhance(0.92 + 0.08 * math.sin(t * math.pi * 2))
# Twinkle sparkles (alpha modulated by sin)
draw = ImageDraw.Draw(frame, 'RGBA')
for (sx, sy, (r, g, b), phase) in sparkles:
alpha = int((0.4 + 0.6 * (0.5 + 0.5 * math.sin(t * math.pi * 2 + phase))) * 255)
draw.rectangle([sx, sy, sx+1, sy+1], fill=(r, g, b, alpha))
# Cross arms at half alpha
for dx, dy in [(-2, 0), (2, 0), (0, -2), (0, 2)]:
draw.rectangle([sx+dx, sy+dy, sx+dx+1, sy+dy+1], fill=(r, g, b, alpha // 2))
frame.save(f'/tmp/frames/frame-{i:02d}.png')
# 3. Stitch into animated WebP
./scripts/pixellab.mjs webp \
--in-dir /tmp/frames \
--out public/assets/games/mygame/thumbnail-animated.webp \
--duration 120
# 4. Update registry.ts:
# thumbnailUrl: "/assets/games/mygame/thumbnail-animated.webp",
Why this beats animate-with-text-v3 for this use case:
animate-with-text-v3 (for characters and genuine motion)Use this when you want actual character motion (walk/run/attack cycles) or genuine concept animation. The API regenerates each frame from the concept, so don't use it when you need to preserve a specific sprite grid.
# Resize if source is >256px (API max)
sips -z 192 256 public/assets/games/mygame/hero-idle.png --out /tmp/hero-256.png
# Generate 8 animation frames
./scripts/pixellab.mjs animate \
--input /tmp/hero-256.png \
--action "walking forward" \
--frames 8 \
--out-dir /tmp/hero-walk
# Stitch into WebP
./scripts/pixellab.mjs webp \
--in-dir /tmp/hero-walk \
--out public/assets/games/mygame/hero-walk.webp \
--duration 120
Generate frames with either approach, then load them as separate textures and play as a Phaser animation:
// boot.ts
for (let i = 0; i < 9; i++) {
this.load.image(`menu-bg-${i}`, `${base}/menu-bg-${i}.png`);
}
// After load (in create())
if (this.textures.exists("menu-bg-0")) {
this.anims.create({
key: "menu-bg-anim",
frames: Array.from({ length: 9 }, (_, i) => ({ key: `menu-bg-${i}` })),
frameRate: 6,
repeat: -1,
});
}
// menu.ts
if (this.anims.exists("menu-bg-anim")) {
this.add
.sprite(width / 2, height / 2, "menu-bg-0")
.setDisplaySize(width, height)
.setAlpha(0.65)
.setDepth(-1)
.play("menu-bg-anim");
}
get_* before downloadingseed parameter for reproducible resultsdetail: "medium detail" is usually best balanceshading: "medium shading" for most game tiles