Skip to content

Commit 433161d

Browse files
committed
feat: implement chromakey background support for improved transparency in sprite processing and animations
1 parent 068dd89 commit 433161d

8 files changed

Lines changed: 280 additions & 23 deletions

File tree

‎skills/pixel-art-generation/SKILL.md‎

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ Use PixelForge MCP tools to generate and process pixel art game assets.
1717
| `forge_thumbnail` | Game thumbnail/screenshot — uses reference sprites |
1818
| `process_sprite` | Post-process existing PNG — crop, bg removal, split |
1919

20+
## Background Removal — IMPORTANT
21+
22+
**Always use `background: "chromakey"` for sprites and animations.** This uses HSV-based green screen removal which is far more reliable than the old compositing equation approach.
23+
24+
- `chromakey` — **BEST** — pure green #00FF00, HSV-based removal, clean edges, no artifacts
25+
- `black` / `white` — legacy compositing equation, can lose dark/light sprite pixels
26+
- Other colors — fallback to compositing equation
27+
2028
## Quick Start
2129

2230
### Single Sprite
@@ -25,20 +33,25 @@ forge_sprite
2533
description: "green slime enemy with horns and glowing eyes"
2634
outputPath: "public/assets/games/rpg/slime.png"
2735
style: "neon"
28-
background: "black"
36+
background: "chromakey"
2937
```
3038

31-
### Animation Frames
39+
### Animation Frames (Reference Chain — Default)
3240
```
3341
forge_animation
3442
description: "green slime enemy"
3543
action: "bouncing up and down"
3644
frames: 4
45+
frameDescriptions: ["idle resting on ground", "compressing flat", "stretching upward", "at peak of bounce"]
3746
outputPrefix: "public/assets/games/rpg/slime"
38-
names: ["bounce-0", "bounce-1", "bounce-2", "bounce-3"]
47+
names: ["idle", "compress", "stretch", "peak"]
3948
style: "neon"
49+
background: "chromakey"
4050
```
41-
Returns individual frame files: `slime-bounce-0.png`, `slime-bounce-1.png`, etc.
51+
52+
**Reference chain mode (default):** Generates frame 0 first, then uses it as a reference image for all subsequent frames. This ensures consistent proportions, colors, and style across all frames. Each frame is a separate API call.
53+
54+
To use the legacy sprite-sheet mode instead: `useReferenceChain: false`
4255

4356
### Background
4457
```
@@ -58,6 +71,14 @@ forge_thumbnail
5871
aspect: "4:3"
5972
```
6073

74+
## Animation Best Practices
75+
76+
1. **Always provide `frameDescriptions`** — explicit per-frame descriptions produce much better results than a generic action
77+
2. **Use `chromakey` background** — prevents transparency artifacts
78+
3. **Reference chain is default** — generates consistent frames by using frame 0 as reference
79+
4. **For inconsistent results, try `useReferenceChain: false` with `useTemplate: true`** — grid template mode as fallback
80+
5. **Keep frame count reasonable** — 3-4 frames per animation state is ideal
81+
6182
## Style Presets
6283

6384
| Style | Best For | Look |
@@ -72,16 +93,15 @@ forge_thumbnail
7293

7394
| Alias | Speed | Quality | Notes |
7495
|-------|-------|---------|-------|
75-
| `gemini-pro` | Medium | Best | Default — best for final assets |
96+
| `nano-banana` | Fast | Good | Default — best balance |
97+
| `pro` | Medium | Best | Best quality, use for hero assets |
7698
| `gemini-flash` | Fast | Good | Good for quick iteration |
77-
| `imagen` | Medium | High | Supports negative prompts |
78-
| `imagen-fast` | Fast | Good | Fastest option |
7999

80100
## Guidelines
81101

82-
1. **forge_sprite and forge_animation auto-process** — output is always cropped, transparent, and ready to use
83-
2. **forge_background outputs raw** — no cropping, full size for game scenes
84-
3. **Use references for thumbnails** — pass actual game sprites so thumbnail matches the game's look
85-
4. **Choose aspect ratio wisely** — 1:1 for sprites, 4:3 for sheets, 3:4 or 16:9 for backgrounds
102+
1. **Always use `chromakey` background** for sprites and animations
103+
2. **forge_sprite and forge_animation auto-process** — output is always cropped, transparent, and ready to use
104+
3. **forge_background outputs raw** — no cropping, full size for game scenes
105+
4. **Use references for thumbnails** — pass actual game sprites so thumbnail matches the game's look
86106
5. **After generating, verify with Read tool** — Claude Code can display PNG files natively
87107
6. **Phaser sprite sizing** — generated images are large (~1000px), always use `setDisplaySize()` in game code

‎skills/sprite-processing/SKILL.md‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ Remove background and auto-crop:
1414
process_sprite
1515
inputPath: "raw-enemy.png"
1616
outputPath: "public/assets/games/rpg/enemy.png"
17-
background: "black"
17+
background: "chromakey"
1818
square: true
1919
```
2020

21+
**Prefer `chromakey` background** — uses HSV-based green screen removal. Much cleaner than black/white.
22+
2123
## Split Sprite Sheet
2224

2325
Split a horizontal sprite sheet into individual frames:
@@ -27,7 +29,7 @@ process_sprite
2729
outputPath: "public/assets/games/rpg/hero"
2830
split: true
2931
names: ["idle", "walk-1", "walk-2", "attack"]
30-
background: "white"
32+
background: "chromakey"
3133
square: true
3234
```
3335
Outputs: `hero-idle.png`, `hero-walk-1.png`, `hero-walk-2.png`, `hero-attack.png`
@@ -53,11 +55,20 @@ process_sprite
5355
skipCrop: true
5456
```
5557

58+
## Background Removal Methods
59+
60+
| Background | Method | Best For |
61+
|-----------|--------|----------|
62+
| `chromakey` | HSV green screen | **BEST** — cleanest edges, no artifacts |
63+
| `black` | Compositing equation | Dark-themed sprites |
64+
| `white` | Compositing equation | Light-themed sprites |
65+
| `auto` | Edge detection | Unknown backgrounds |
66+
5667
## Options Reference
5768

5869
| Option | Default | Description |
5970
|--------|---------|-------------|
60-
| `background` | auto-detect | `"black"`, `"white"`, or `"auto"` |
71+
| `background` | auto-detect | `"chromakey"`, `"black"`, `"white"`, or `"auto"` |
6172
| `threshold` | 20 | Color detection sensitivity (0-255) |
6273
| `square` | false | Pad output to square dimensions |
6374
| `padding` | 2 | Pixels of padding around content |

‎src/pipeline/background.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface BgEntry {
1010
export const BG_COLOR_MAP: Record<Background, BgEntry> = {
1111
black: { rgb: { r: 0, g: 0, b: 0 }, hex: '#000000', name: 'black' },
1212
white: { rgb: { r: 255, g: 255, b: 255 }, hex: '#FFFFFF', name: 'white' },
13+
chromakey: { rgb: { r: 0, g: 255, b: 0 }, hex: '#00FF00', name: 'chroma key green' },
1314
forest: { rgb: { r: 74, g: 103, b: 65 }, hex: '#4A6741', name: 'forest green' },
1415
sky: { rgb: { r: 74, g: 107, b: 138 }, hex: '#4A6B8A', name: 'sky blue' },
1516
dungeon: { rgb: { r: 42, g: 42, b: 42 }, hex: '#2A2A2A', name: 'dungeon gray' },
@@ -63,6 +64,9 @@ export function bgPromptFragment(bgKey: Background): string {
6364
if (bgKey === 'black' || bgKey === 'white') {
6465
return `Pure ${bgKey} background, completely flat solid ${bgKey} with no variation.`;
6566
}
67+
if (bgKey === 'chromakey') {
68+
return `Solid flat chroma key green background, EXACT hex #00FF00 (RGB 0, 255, 0) with NO gradients, NO noise, NO texture. The sprite must have a bold dark pixel outline 2-3 pixels wide around the entire shape.`;
69+
}
6670
return `Solid ${entry.name} background (${entry.hex}), completely flat uniform color with no variation.`;
6771
}
6872

‎src/pipeline/image-ops.ts‎

Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,95 @@ export function makeTransparentColor(
117117
return out;
118118
}
119119

120+
/**
121+
* HSV-based chromakey green background removal.
122+
* Detects green pixels via Hue ±22° of 120°, Sat > 0.3, Val > 0.3
123+
* and sets them transparent. Edge pixels get soft alpha via distance
124+
* from the green hue center. Much more reliable than compositing eq.
125+
*/
126+
export function removeChromakeyGreen(
127+
pixels: Buffer,
128+
width: number,
129+
height: number,
130+
): Buffer {
131+
const out = Buffer.from(pixels);
132+
133+
for (let i = 0; i < width * height * 4; i += 4) {
134+
const r = out[i]!;
135+
const g = out[i + 1]!;
136+
const b = out[i + 2]!;
137+
138+
// Convert RGB to HSV
139+
const rn = r / 255;
140+
const gn = g / 255;
141+
const bn = b / 255;
142+
const max = Math.max(rn, gn, bn);
143+
const min = Math.min(rn, gn, bn);
144+
const delta = max - min;
145+
146+
let h = 0;
147+
if (delta > 0) {
148+
if (max === rn) h = 60 * (((gn - bn) / delta) % 6);
149+
else if (max === gn) h = 60 * ((bn - rn) / delta + 2);
150+
else h = 60 * ((rn - gn) / delta + 4);
151+
if (h < 0) h += 360;
152+
}
153+
const s = max === 0 ? 0 : delta / max;
154+
const v = max;
155+
156+
// Green detection: hue 98°-142° (120° ± 22°), sat > 0.3, val > 0.3
157+
const hueCenter = 120;
158+
const hueTolerance = 22;
159+
const hueDist = Math.abs(h - hueCenter);
160+
161+
if (hueDist <= hueTolerance && s > 0.3 && v > 0.3) {
162+
// Pure green background — fully transparent
163+
out[i] = 0;
164+
out[i + 1] = 0;
165+
out[i + 2] = 0;
166+
out[i + 3] = 0;
167+
} else if (hueDist <= hueTolerance + 10 && s > 0.15 && v > 0.2) {
168+
// Edge zone — soft alpha ramp based on distance from green
169+
const edgeDist = Math.max(0, hueTolerance + 10 - hueDist) / 10;
170+
const satFade = s > 0.3 ? 1.0 : (s - 0.15) / 0.15;
171+
const fade = edgeDist * satFade;
172+
const alpha = Math.max(0, 1 - fade);
173+
174+
if (alpha < 0.05) {
175+
out[i] = 0;
176+
out[i + 1] = 0;
177+
out[i + 2] = 0;
178+
out[i + 3] = 0;
179+
} else {
180+
out[i + 3] = Math.round(alpha * 255);
181+
}
182+
}
183+
// else: keep pixel as-is (fully opaque, alpha stays 255)
184+
}
185+
186+
// Edge softening: average alpha with 4 neighbors for smooth edges
187+
const softened = Buffer.from(out);
188+
for (let y = 1; y < height - 1; y++) {
189+
for (let x = 1; x < width - 1; x++) {
190+
const i = (y * width + x) * 4;
191+
const a = out[i + 3]!;
192+
// Only soften partially transparent edge pixels
193+
if (a > 0 && a < 255) {
194+
const neighbors = [
195+
out[((y - 1) * width + x) * 4 + 3]!,
196+
out[((y + 1) * width + x) * 4 + 3]!,
197+
out[(y * width + x - 1) * 4 + 3]!,
198+
out[(y * width + x + 1) * 4 + 3]!,
199+
];
200+
const avg = (a + neighbors[0] + neighbors[1] + neighbors[2] + neighbors[3]) / 5;
201+
softened[i + 3] = Math.round(avg);
202+
}
203+
}
204+
}
205+
206+
return softened;
207+
}
208+
120209
/**
121210
* Compositing-equation based soft alpha background removal.
122211
* Inspired by godogen's rembg_matting.py — produces smooth edges and
@@ -129,8 +218,8 @@ export function removeBackgroundColor(
129218
bg: BgColor,
130219
opts?: { noiseFloor?: number; solidThreshold?: number }
131220
): Buffer {
132-
const noiseFloor = opts?.noiseFloor ?? 0.08;
133-
const solidThreshold = opts?.solidThreshold ?? 0.55;
221+
const noiseFloor = opts?.noiseFloor ?? 0.06;
222+
const solidThreshold = opts?.solidThreshold ?? 0.35;
134223
const out = Buffer.from(pixels);
135224

136225
for (let i = 0; i < width * height * 4; i += 4) {
@@ -457,7 +546,7 @@ export function pixelateDownscale(
457546

458547
const di = (dy * dstW + dx) * 4;
459548
const totalPixels = (sx2 - sx1) * (sy2 - sy1);
460-
if (count > 0 && count >= totalPixels * 0.3) {
549+
if (count > 0 && count >= totalPixels * 0.15) {
461550
// Enough opaque pixels — this is content
462551
out[di] = Math.round(r / count);
463552
out[di + 1] = Math.round(g / count);
@@ -480,13 +569,19 @@ export function processSpriteColor(
480569
skipCrop?: boolean;
481570
skipTransparent?: boolean;
482571
size?: number;
572+
chromakey?: boolean;
483573
}
484574
): ImageData {
485575
const pad = opts.padding ?? 2;
486576
let { width, height, pixels } = img;
487577

488578
if (!opts.skipTransparent) {
489-
pixels = removeBackgroundColor(pixels, width, height, bgColor);
579+
// Use HSV chromakey removal for green backgrounds, compositing eq for others
580+
if (opts.chromakey) {
581+
pixels = removeChromakeyGreen(pixels, width, height);
582+
} else {
583+
pixels = removeBackgroundColor(pixels, width, height, bgColor);
584+
}
490585
}
491586

492587
if (!opts.skipCrop) {
@@ -564,6 +659,7 @@ export function splitAndProcess(
564659
expectedFrames?: number;
565660
maxSize?: number;
566661
bgColorHint?: BgColor;
662+
chromakey?: boolean;
567663
}
568664
): ImageData[] {
569665
const pad = opts.padding ?? 4;
@@ -606,8 +702,12 @@ export function splitAndProcess(
606702
const cy2 = Math.min(img.height, b.y2 + pad);
607703

608704
let sprite = cropPixels(img.pixels, img.width, cx1, cy1, cx2, cy2);
609-
// Use compositing-equation soft alpha for accurate bg removal
610-
sprite.pixels = removeBackgroundColor(sprite.pixels, sprite.width, sprite.height, bgColor);
705+
// Remove background: HSV chromakey for green, compositing equation for others
706+
if (opts.chromakey) {
707+
sprite.pixels = removeChromakeyGreen(sprite.pixels, sprite.width, sprite.height);
708+
} else {
709+
sprite.pixels = removeBackgroundColor(sprite.pixels, sprite.width, sprite.height, bgColor);
710+
}
611711

612712
// Tight crop to non-transparent pixels
613713
let x1t = sprite.width,

‎src/pipeline/prompt-builder.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,33 @@ export function detailHintForSize(size: number): string {
5555
return 'Detailed pixel art, fine features allowed, rich color palette.';
5656
}
5757

58+
// ── Single animation frame prompt (reference-chain mode) ─────────────────
59+
60+
export function buildAnimationFramePrompt(
61+
description: string,
62+
frameDescription: string,
63+
isFirstFrame: boolean,
64+
style: Style = 'clean',
65+
bg: Background = 'black',
66+
size: number = 48
67+
): string {
68+
const bgEntry = BG_COLOR_MAP[bg];
69+
const refNote = isFirstFrame
70+
? ''
71+
: ' Must match the reference image exactly in style, proportions, colors, and outline thickness — only the pose/state changes.';
72+
const parts = [
73+
`${description}, ${frameDescription}.${refNote}`,
74+
`${STYLE_PRESETS[style]}.`,
75+
bgEntry ? `Centered on a solid ${bgEntry.name} (${bgEntry.hex}) background.` : '',
76+
PIXEL_ART_CORE.join(', ') + '.',
77+
SPRITE_RULES.join(', ') + '.',
78+
].filter(Boolean);
79+
const hint = detailHintForSize(size);
80+
if (hint) parts.push(hint);
81+
parts.push(NEGATIVE_ALWAYS);
82+
return parts.join(' ');
83+
}
84+
5885
// ── Sprite prompt (godogen-simplified) ────────────────────────────────────
5986

6087
export function buildSpritePrompt(

0 commit comments

Comments
 (0)