# Pattern Designer Agent Skill

Generate beautiful motif patterns programmatically via URL-based API.

## Quick Start

**Generate a pattern in 3 steps:**
1. Build config JSON (canvas size, icons, colors, gradients)
2. Encode to base64
3. Open URL: `http://localhost:3000/pattern-designer?config=<base64>&autodownload=true&format=png`

Pattern auto-generates and downloads when `autodownload=true`.

## URL Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `config` | base64 | Yes | Encoded pattern configuration |
| `autodownload` | boolean | No | Auto-download when complete (default: false) |
| `format` | string | No | Export format: `png`, `jpeg`, `svg` (default: png) |

**Security:** Autodownload ONLY triggers when explicitly set to `true`.

## Configuration Schema

```typescript
{
  config: {
    canvasWidth: number;        // Pattern width in pixels
    canvasHeight: number;       // Pattern height in pixels
    backgroundType: 'solid' | 'gradient';
    backgroundColor: string;    // Hex color (e.g., '#2D2738')
    backgroundGradient?: {
      type: 'linear' | 'radial';
      angle?: number;           // For linear: 0-360 degrees
      stops: Array<{
        position: number;       // 0-100 (percentage)
        color: string;          // Hex color
      }>;
    };
    density: number;            // Spacing multiplier (0.5-10.0, default 1.5)
    icons: Array<{
      id: string;               // Unique identifier
      type: 'emoji' | 'lucide' | 'upload';
      emojiChar?: string;       // For emoji type
      codepoints?: string;      // For emoji type (hex, e.g., '1f3ca')
      lucideName?: string;      // For lucide type (e.g., 'heart')
      dataUrl?: string;         // For upload type (data URL)
    }>;
    frequencyRatios: {
      [iconId: string]: number; // Weight for distribution (1-10)
    };
    placedIcons: [];            // Leave empty, generator fills this
  },
  iconControls: {
    [iconId: string]: {
      sizeMin: number;          // Minimum icon size in pixels
      sizeMax: number;          // Maximum icon size in pixels
      rotationMin: number;      // Min rotation in degrees (0-360)
      rotationMax: number;      // Max rotation in degrees (0-360)
      opacityMin: number;       // Min opacity (0.0-1.0)
      opacityMax: number;       // Max opacity (0.0-1.0)
      color?: string;           // Hex color overlay (optional)
    };
  }
}
```

## Encoding/Decoding

### ✅ CORRECT Method (Node.js/Browser)

```javascript
// 1. Build your config object
const config = {
  config: { /* pattern config */ },
  iconControls: { /* icon controls */ }
};

// 2. Convert to JSON string
const json = JSON.stringify(config);

// 3. Encode to UTF-8 bytes (REQUIRED for emojis!)
const encoder = new TextEncoder();
const bytes = encoder.encode(json);

// 4. Convert bytes to base64
const base64 = Buffer.from(bytes).toString('base64');  // Node.js
// OR in browser: btoa(String.fromCharCode(...bytes));

// 5. Use in URL
const url = `http://localhost:3000/pattern-designer?config=${base64}&autodownload=true&format=png`;
```

### ❌ WRONG Methods (Will Fail)

**Don't use plain btoa():**
```javascript
// ❌ This corrupts emojis and multi-byte UTF-8
const base64 = btoa(JSON.stringify(config));
```

**Don't use encodeURIComponent():**
```javascript
// ❌ This is for URL encoding, not base64
const encoded = encodeURIComponent(JSON.stringify(config));
```

**Don't encode with external tools:**
```javascript
// ❌ Shell pipes, online encoders may add control characters
// Always encode programmatically in the same environment
```

### Validation

After encoding, decode it back to verify:
```javascript
const decoded = new TextDecoder().decode(
  Buffer.from(base64, 'base64')
);
const parsed = JSON.parse(decoded);
console.log('Config valid:', parsed.config && parsed.iconControls);
```

**⚠️ Critical:** If you see "bad control character" or "unexpected token" errors, your encoding is wrong. Use the method above exactly.

## Completion Signals

Pattern designer sets DOM attributes when done (for agent polling):

```html
<body 
  data-pattern-status="complete"
  data-result-url="blob:http://localhost:3000/abc123"
  data-result-format="png"
  data-timestamp="1234567890"
>
```

**Status values:**
- `pending` - Initial state
- `generating` - Pattern rendering in progress
- `complete` - Done, result available
- `error` - Generation failed

**Error attributes (when status=error):**
- `data-error` - Error message
- `data-error-hint` - Helpful troubleshooting hint

**Common errors:**
- "control character" / "JSON.parse" → Config encoding issue (use UTF-8)
- "Canvas" → Canvas rendering failed (icons not loaded)
- "blob" → Export failed (pattern too large or icons missing)
- "timeout" → Operation timed out (reduce canvas size or icon count)

**Agent polling pattern:**
```javascript
// Open URL with config
window.open(url);

// Poll for completion
const interval = setInterval(() => {
  const status = document.body.dataset.patternStatus;
  if (status === 'complete') {
    const resultUrl = document.body.dataset.resultUrl;
    const format = document.body.dataset.resultFormat;
    // Download available at resultUrl
    clearInterval(interval);
  } else if (status === 'error') {
    const error = document.body.dataset.error;
    const hint = document.body.dataset.errorHint;
    console.error('Generation failed:', error);
    if (hint) console.error('Hint:', hint);
    clearInterval(interval);
  }
}, 500);
```

**Event-based pattern (better than polling):**
```javascript
// Listen for completion event
document.addEventListener('pattern-designer-status', (event) => {
  const { status, resultUrl, format, error } = event.detail;
  
  if (status === 'complete') {
    console.log('Pattern ready:', resultUrl, format);
    // Download and send to user
  } else if (status === 'error') {
    console.error('Pattern generation failed:', error);
    const hint = document.body.dataset.errorHint;
    // Report to user with helpful hint
    sendMessage(`Failed to generate pattern: ${error}${hint ? `\n\nTip: ${hint}` : ''}`);
  }
});

// Then open URL
window.open(url);
```

## Icon Types

**⚠️ For Agent Automation: Use Lucide icons instead of emojis to avoid encoding issues!**

Emojis are multi-byte UTF-8 characters that can be corrupted during encoding/transmission. Lucide icons are simple ASCII strings that work reliably across all platforms.

### Lucide Icons (~1,900 available) - **RECOMMENDED FOR AGENTS**

```javascript
{
  id: 'heart',
  type: 'lucide',
  lucideName: 'heart'  // Simple ASCII string - no encoding issues!
}
```

**Why Lucide for agents:**
- ✅ ASCII strings - no multi-byte UTF-8 encoding
- ✅ Consistent across all platforms
- ✅ ~1,900 high-quality icons
- ✅ Color overlays work perfectly
- ✅ No emoji font dependencies

**Available icons:** heart, star, circle, square, triangle, sun, moon, cloud, sparkles, etc.

**Browse:** https://lucide.dev/icons

### Emojis (~1,800 available) - **USE ONLY IF NECESSARY**

```javascript
{
  id: 'star',
  type: 'emoji',
  emojiChar: '⭐',
  codepoints: '2b50'  // Unicode codepoint(s) in hex
}
```

**Emoji encoding pitfalls for agents:**
- ❌ Multi-byte UTF-8 (3-4 bytes per emoji)
- ❌ Can be corrupted by improper base64 encoding
- ❌ Some emojis have skin tone modifiers (extra bytes)
- ❌ ZWJ sequences (zero-width joiners) don't render
- ⚠️ **MUST use TextEncoder/TextDecoder**, never plain btoa/atob

**Finding codepoints:**
- Search emoji picker in browser
- Use unicode database: https://unicode.org/emoji/charts/full-emoji-list.html
- Extract from emoji: `'⭐'.codePointAt(0).toString(16)` → `'2b50'`

**Filtered emojis:**
- Only fully-qualified emojis (clean rendering)
- No ZWJ sequences (👨‍👩‍👧 multi-person emojis)
- No skin tone modifiers (prevents white boxes)

### Uploaded Images

```javascript
{
  id: 'custom-logo',
  type: 'upload',
  dataUrl: 'data:image/png;base64,iVBORw0KG...'
}
```

**Supported formats:** PNG, JPEG, SVG, GIF, WebP

## Gradients

**Linear gradient (top to bottom, angle 180°):**
```javascript
backgroundGradient: {
  type: 'linear',
  angle: 180,
  stops: [
    { position: 0, color: '#ff6b6b' },   // Top
    { position: 100, color: '#1a1625' }  // Bottom
  ]
}
```

**Radial gradient (center to edges):**
```javascript
backgroundGradient: {
  type: 'radial',
  stops: [
    { position: 0, color: '#ba240f' },   // Center
    { position: 100, color: '#170204' }  // Edges
  ]
}
```

**Multiple stops (3+ colors):**
```javascript
stops: [
  { position: 0, color: '#ff0000' },   // Start
  { position: 50, color: '#00ff00' },  // Middle
  { position: 100, color: '#0000ff' } // End
]
```

## Example Configs

### Swimming Pool Banner (1200x400)

```javascript
{
  config: {
    canvasWidth: 1200,
    canvasHeight: 400,
    backgroundType: 'gradient',
    backgroundColor: '#ffffff',
    backgroundGradient: {
      type: 'linear',
      angle: 180,
      stops: [
        { position: 0, color: '#87CEEB' },   // Sky blue
        { position: 100, color: '#0077BE' }  // Ocean blue
      ]
    },
    density: 1.5,
    icons: [
      { id: 'pool', type: 'emoji', emojiChar: '🏊', codepoints: '1f3ca' },
      { id: 'wave', type: 'emoji', emojiChar: '🌊', codepoints: '1f30a' },
      { id: 'beach', type: 'emoji', emojiChar: '🏖️', codepoints: '1f3d6' },
      { id: 'mermaid', type: 'emoji', emojiChar: '🧜', codepoints: '1f9dc' }
    ],
    frequencyRatios: {
      pool: 3,      // More swimmers
      wave: 2,      // Some waves
      beach: 1,     // Occasional beach
      mermaid: 2    // Some mermaids
    },
    placedIcons: []
  },
  iconControls: {
    pool: {
      sizeMin: 40,
      sizeMax: 60,
      rotationMin: -15,
      rotationMax: 15,
      opacityMin: 0.8,
      opacityMax: 1.0
    },
    wave: {
      sizeMin: 35,
      sizeMax: 50,
      rotationMin: 0,
      rotationMax: 0,
      opacityMin: 0.6,
      opacityMax: 0.9
    },
    beach: {
      sizeMin: 45,
      sizeMax: 70,
      rotationMin: -10,
      rotationMax: 10,
      opacityMin: 0.7,
      opacityMax: 1.0
    },
    mermaid: {
      sizeMin: 50,
      sizeMax: 75,
      rotationMin: -20,
      rotationMax: 20,
      opacityMin: 0.8,
      opacityMax: 1.0
    }
  }
}
```

### Star Field (800x600)

```javascript
{
  config: {
    canvasWidth: 800,
    canvasHeight: 600,
    backgroundType: 'gradient',
    backgroundColor: '#000000',
    backgroundGradient: {
      type: 'radial',
      stops: [
        { position: 0, color: '#1a1a2e' },
        { position: 100, color: '#000000' }
      ]
    },
    density: 2.0,
    icons: [
      { id: 'star', type: 'emoji', emojiChar: '⭐', codepoints: '2b50' }
    ],
    frequencyRatios: { star: 1 },
    placedIcons: []
  },
  iconControls: {
    star: {
      sizeMin: 20,
      sizeMax: 50,
      rotationMin: 0,
      rotationMax: 360,
      opacityMin: 0.4,
      opacityMax: 1.0,
      color: '#FFFFFF'  // White stars
    }
  }
}
```

## Agent Workflow (Complete Automation)

**Agents should automate the entire flow, not ask users to open links!**

### Step-by-step:

**1. Build the config:**
```javascript
const config = {
  config: { /* canvas, icons, gradients */ },
  iconControls: { /* size, rotation, opacity, color */ }
};
```

**2. Encode properly:**
```javascript
const json = JSON.stringify(config);
const encoder = new TextEncoder();
const bytes = encoder.encode(json);
const base64 = Buffer.from(bytes).toString('base64');
```

**3. Construct URL (CRITICAL: Use plain `&`, not `&amp;`):**
```javascript
// ✅ CORRECT
const url = `http://localhost:3000/pattern-designer?config=${base64}&autodownload=true&format=png`;

// ❌ WRONG - DO NOT URL-encode the base64
const url = `http://localhost:3000/pattern-designer?config=${encodeURIComponent(base64)}...`; // NO!

// ❌ WRONG - DO NOT use HTML entities
const url = `...?config=${base64}&amp;autodownload=true...`; // NO!

// ❌ WRONG - DO NOT add extra encoding
const url = encodeURI(`...?config=${base64}...`); // NO!
```

**Important:** Base64 is already URL-safe. Don't encode it again or the browser will double-decode and corrupt emojis.

**4. Use browser automation to fetch the image:**

```python
# Using browser tool (Playwright, Puppeteer, etc.)
page.goto(url)

# Wait for completion signal (download will start automatically)
page.wait_for_function("""
  () => document.body.dataset.patternStatus === 'complete' || 
        document.body.dataset.patternStatus === 'error'
""", timeout=10000)

# Check if successful
status = page.evaluate("() => document.body.dataset.patternStatus")
if status == 'error':
    error = page.evaluate("() => document.body.dataset.error")
    hint = page.evaluate("() => document.body.dataset.errorHint")
    raise Exception(f"Pattern generation failed: {error}. {hint}")

# Get the blob URL from DOM
result_url = page.evaluate("""
  () => document.body.dataset.resultUrl
""")

# Download the blob
blob_data = page.evaluate(f"""
  async () => {{
    const response = await fetch('{result_url}');
    const blob = await response.blob();
    const reader = new FileReader();
    return new Promise(resolve => {{
      reader.onload = () => resolve(reader.result.split(',')[1]);
      reader.readAsDataURL(blob);
    }});
  }}
""")

# Decode base64 to binary
import base64
image_bytes = base64.b64decode(blob_data)

# Save or send directly
with open('pattern.png', 'wb') as f:
    f.write(image_bytes)
```

**5. Send to user:**
```python
# Slack example
client.files_upload(
    channels='#general',
    file=image_bytes,
    filename='pattern.png',
    title='Your custom pattern',
    initial_comment='Here\'s your swimming pool banner!'
)
```

### Alternative: Direct HTTP approach (no browser needed)

If your agent can't use browser automation, generate a simple HTML page that loads the pattern and provides a download endpoint:

```html
<!DOCTYPE html>
<html>
<body>
<script>
  // Load pattern designer page in iframe
  const iframe = document.createElement('iframe');
  iframe.src = 'http://localhost:3000/pattern-designer?config=YOUR_BASE64';
  document.body.appendChild(iframe);
  
  // Listen for completion
  setInterval(() => {
    try {
      const status = iframe.contentDocument.body.dataset.patternStatus;
      if (status === 'complete') {
        const url = iframe.contentDocument.body.dataset.resultUrl;
        // Fetch and forward blob
        fetch(url).then(r => r.blob()).then(blob => {
          // Send to your backend endpoint
          const formData = new FormData();
          formData.append('file', blob, 'pattern.png');
          fetch('/upload-pattern', { method: 'POST', body: formData });
        });
      }
    } catch (e) { /* CORS */ }
  }, 500);
</script>
</body>
</html>
```

### For vision-capable agents (iterative design):
1. Generate pattern with config
2. Wait for `data-pattern-status="complete"`
3. Screenshot the canvas
4. Analyze visually
5. Iterate: adjust density, colors, sizes
6. Regenerate until satisfied
7. Final export and send to user

**Performance:**
- Pattern generation: <1s for 100 icons
- Canvas rendering: ~2s (includes emoji font loading)
- Export (1200x800 PNG): ~1-2s
- Total workflow: ~4-5s from URL open to download

## Common Emoji Codepoints

| Emoji | Name | Codepoint |
|-------|------|-----------|
| ⭐ | Star | 2b50 |
| ❤️ | Heart | 2764 |
| 🌊 | Wave | 1f30a |
| 🏖️ | Beach | 1f3d6 |
| 🏊 | Swimmer | 1f3ca |
| 🧜 | Merperson | 1f9dc |
| 🌸 | Cherry Blossom | 1f338 |
| 🍕 | Pizza | 1f355 |
| 🎵 | Musical Note | 1f3b5 |
| 🔥 | Fire | 1f525 |
| ✨ | Sparkles | 2728 |
| 🌈 | Rainbow | 1f308 |

**Find more:** https://unicode.org/emoji/charts/full-emoji-list.html

## Troubleshooting

**Pattern not loading:**
- Check console for "Decoded config:" log
- Verify base64 encoding uses UTF-8 (TextEncoder)
- Ensure config structure matches schema

**Emoji shows as �:**
- Use UTF-8 encoding (not plain btoa)
- Avoid ZWJ sequences and skin tone modifiers
- Check codepoint is correct

**Export is corrupted:**
- Wait for `data-pattern-status="complete"` before accessing result
- Check blob size > 0
- Verify format parameter matches expected type

**Autodownload not working:**
- Must explicitly set `autodownload=true` (security)
- Check completion signal in DOM
- Verify no console errors during export

## Source Code

- **Main page:** `src/app/pattern-designer/page.tsx`
- **Generator:** `src/lib/pattern-designer/generator.ts`
- **Export:** `src/lib/pattern-designer/export.ts`
- **URL config:** `src/lib/pattern-designer/url-config.ts`
- **Completion signals:** `src/lib/pattern-designer/completion-signal.ts`

## Known Limitations

1. **SVG exports don't respect color overlays** (Issue #1)
   - PNG/JPEG: color overlays work ✅
   - SVG: shows original icon colors ❌
   
2. **No layer effects yet** (strokes, shadows, glows, bevel)
   - Designed but not implemented
   - See design spec: `docs/superpowers/specs/2026-03-31-agent-api-and-layer-effects-design.md`

3. **Emoji font loading timing**
   - Fonts load async, exports wait 2s
   - May need longer wait for slow connections

## Support

- **Issues:** https://github.com/n8m8/blog/issues
- **Dev server:** http://localhost:3000/pattern-designer
- **Documentation:** This file

---

**Last updated:** 2026-03-31
**Version:** 1.0 (Agent API Phase 1 complete)
