banner-creator

Public
0

Repository: resciencelab/opc-skills

Log in or sign up to clone this skill.

R
resciencelab
Imported Feb 28, 2026

Low Risk

No security issues found

INFO

Skill manifest does not include a 'license' field. Specifying a license helps users understand usage terms.

Remediation Add 'license' field to SKILL.md frontmatter (e.g., MIT, Apache-2.0)

Scanned in 0.008s

Description

Create banners using AI image generation. Discuss format/style, generate variations, iterate with user feedback, crop to target ratio. Use when user wants to create a banner, header, hero image, cover image, GitHub banner, Twitter header, or readme banner.

Skill Files

Download .zip
SKILL.md
# Banner Creator Skill

Create professional banners through AI image generation with an iterative design process.

## Prerequisites

**Required API Keys (set in environment):**
- `GEMINI_API_KEY` - Get from [Google AI Studio](https://aistudio.google.com/apikey)

**Required Skills:**
- `nanobanana` - AI image generation (Gemini 3 Pro Image)



## File Output Location

All generated files should be saved to the `.skill-archive` directory:

```
.skill-archive/banner-creator/<yyyy-mm-dd-summaryname>/
```

**Example:**
```
.skill-archive/banner-creator/2026-01-19-opc-banner/
  banner-01.png
  banner-02.png
  ...
  banner-03-cropped.png
  preview.html
```

## Workflow

### Step 1: Discovery & Requirements

Before generating, gather requirements from user:

**Ask about:**
1. **Purpose** - Where will the banner be used?
   - GitHub README
   - Twitter/X header
   - LinkedIn banner
   - Website hero
   - YouTube channel art

2. **Target ratio/size** - See [references/formats.md](./references/formats.md):
   - `2:1` (1280x640) - GitHub README
   - `3:1` (1500x500) - Twitter header
   - `16:9` (1920x1080) - Website hero

3. **Style preference**:
   - Match existing logo/brand?
   - Pixel art / 8-bit retro
   - Minimalist / flat design
   - Gradient / modern
   - Illustrated / artistic

4. **Content elements**:
   - Brand name / project name?
   - Tagline / slogan?
   - Logo character to include?

5. **Color preferences**:
   - Existing brand colors?
   - Let AI decide?

**Wait for user confirmation before proceeding!**

### Step 2: Generate Banner Variations

Generate 20 banner variations using the `nanobanana` skill:

```bash
# Generate single banner
python3 <nanobanana_skill_dir>/scripts/generate.py "{style} banner for {brand}, {description}, {text elements}" \
  --ratio 21:9 -o .skill-archive/banner-creator/<date-name>/banner-01.png

# Batch generate 20 banners
python3 <nanobanana_skill_dir>/scripts/batch_generate.py "{style} banner for {brand}, {description}, {text elements}" \
  -n 20 --ratio 21:9 -d .skill-archive/banner-creator/<date-name> -p banner
```

**Guidelines:**
- Generate at `21:9` ratio (widest available), crop later to target
- Use batch_generate.py for multiple variations (includes auto-delay)
- Use sequential naming: `banner-01.png`, `banner-02.png`, etc.

**Image Editing (for incorporating existing logo):**
```bash
python3 <nanobanana_skill_dir>/scripts/generate.py "add {logo character} to the left side of the banner" \
  -i /path/to/existing-logo.png --ratio 21:9 -o banner-with-logo.png
```

### Step 3: Create HTML Preview

Copy the preview template and open in browser:

```bash
cp <skill_dir>/templates/preview.html .skill-archive/banner-creator/<yyyy-mm-dd-summaryname>/preview.html
```

Then open in default browser:

```bash
open .skill-archive/banner-creator/<yyyy-mm-dd-summaryname>/preview.html
```

**IMPORTANT:** Update the HTML to include the correct number of banners generated.

### Step 4: Iterate with User

Ask user which banners they prefer:
- "Which banners do you like? (e.g., #3, #7, #15)"
- "What do you like about them?"
- "Any changes you'd want?"

Based on feedback:
1. Generate 10-20 more variations of favorite styles
2. Use naming: `banner-{original}-v{n}.png` (e.g., `banner-03-v1.png`)
3. Update HTML preview
4. Repeat until user selects final banner

### Step 5: Crop to Target Ratio

Once user approves a banner, crop to target size:

```bash
python3 <skill_dir>/scripts/crop_banner.py {input.png} {output.png} --ratio 2:1 --width 1280
```

**Common targets:**
- GitHub README: `--ratio 2:1 --width 1280` → 1280x640
- Twitter header: `--ratio 3:1 --width 1500` → 1500x500
- Website hero: `--ratio 16:9 --width 1920` → 1920x1080

### Step 6: Deliver Final Assets

Present final deliverables:

```
## Final Banner Assets

| File | Description | Size |
|------|-------------|------|
| banner-03.png | Original (21:9) | 2016x864 |
| banner-03-cropped.png | GitHub README (2:1) | 1280x640 |

All files saved to: `.skill-archive/banner-creator/<yyyy-mm-dd-summaryname>/`
Copy final banner to user's desired location.
```

## Quick Reference

### Common Prompt Patterns

**With Text:**
```
Wide banner for {brand}, {style} style, featuring "{text}" prominently displayed, {colors}, {scene/elements}
```

**With Character:**
```
Wide banner featuring {character description}, {style} style, {scene}, text "{brand name}" on {position}, {colors}
```

**Abstract/Gradient:**
```
Abstract {style} banner, {colors} gradient, geometric patterns, modern tech feel, text "{brand}" centered
```

**Scene-based:**
```
{Style} illustration banner, {scene description}, {character} in {action}, "{brand}" text overlay, {colors}
```

### Supported Aspect Ratios

Generate at widest ratio, then crop:
- `21:9` - Ultra-wide (recommended for generation)
- `16:9` - Wide
- `3:2` - Standard wide

## References

- [references/formats.md](./references/formats.md) - Common banner sizes by platform
- [examples/opc-banner-creation.md](./examples/opc-banner-creation.md) - Full example conversation
.claude-plugin/plugin.json Reference
{
  "name": "banner-creator",
  "version": "1.0.0",
  "description": "Create banners using AI image generation. Discuss format/style, generate variations, iterate with user feedback, crop to target ratio for GitHub, Twitter, LinkedIn, etc.",
  "author": {
    "name": "ReScienceLab"
  },
  "homepage": "https://github.com/ReScienceLab/opc-skills/tree/main/skills/banner-creator",
  "repository": "https://github.com/ReScienceLab/opc-skills",
  "license": "MIT",
  "keywords": [
    "banner",
    "header",
    "hero image",
    "cover image",
    "create banner",
    "github banner",
    "twitter header",
    "readme banner"
  ],
  "skills": [
    "./SKILL.md"
  ],
  "commands": [
    "./scripts/"
  ]
}
examples/opc-banner-creation.md Reference
# Example: OPC Skills Banner Creation

**User:** Create a banner for opc.dev GitHub README. Should match the existing pixel art logo style.

**Agent:** I'll create a banner for OPC Skills. Let me confirm the requirements:

1. Target size: GitHub README banner (typically 1280x640, 2:1 ratio)
2. Style: Pixel art to match existing crowned king logo
3. Text: "opc.dev" and "Agent Skills"
4. Character: Include the pixel art crowned king from logo

**User:** Yes, that's correct. Please generate some options.

**Agent:** Generating banner variations at 16:9 ratio first...

Here are the generated banners:

![Banner 01](/skills/banner-creator/examples/images/banner-01.png)
![Banner 02](/skills/banner-creator/examples/images/banner-02.png)
![Banner 03](/skills/banner-creator/examples/images/banner-03.png)
![Banner 04](/skills/banner-creator/examples/images/banner-04.png)
![Banner 05](/skills/banner-creator/examples/images/banner-05.png)
![Banner 06](/skills/banner-creator/examples/images/banner-06.png)

**User:** I like the style. But GitHub banners work best at 2:1 ratio. Can you generate some at that ratio?

**Agent:** Generating at 2:1 ratio for GitHub:

![GitHub Banner 01](/skills/banner-creator/examples/images/github-banner-01.png)
![GitHub Banner 02](/skills/banner-creator/examples/images/github-banner-02.png)
![GitHub Banner 03](/skills/banner-creator/examples/images/github-banner-03.png)

**User:** Perfect! Let's use the third one.

**Agent:** Here's the final banner:

![Final Banner](/skills/banner-creator/examples/images/opc-banner-final.png)

The banner is ready to use in your README!
references/formats.md Reference
# Banner Formats Reference

## Platform-Specific Sizes

| Platform | Size (px) | Ratio | Notes |
|----------|-----------|-------|-------|
| **GitHub README** | 1280×640 | 2:1 | Most common for project banners |
| **Twitter/X Header** | 1500×500 | 3:1 | Profile header image |
| **LinkedIn Banner** | 1584×396 | 4:1 | Personal profile background |
| **LinkedIn Company** | 1128×191 | ~6:1 | Company page banner |
| **YouTube Channel** | 2560×1440 | 16:9 | Channel art (safe area: 1546×423) |
| **Facebook Cover** | 820×312 | ~2.6:1 | Personal profile |
| **Discord Server** | 960×540 | 16:9 | Server banner |
| **Website Hero** | 1920×1080 | 16:9 | Full-width hero section |
| **Website Hero (tall)** | 1920×800 | 2.4:1 | Shorter hero section |
| **Email Header** | 600×200 | 3:1 | Newsletter headers |
| **Product Hunt** | 1270×760 | ~1.67:1 | Gallery images |

## Generation Strategy

Since `nano-banana-pro` supports these ratios:
- `1:1`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `16:9`, `21:9`

**Recommended approach:**
1. Generate at `21:9` (widest available)
2. Crop to target ratio using `crop_banner.py`

This ensures:
- Maximum flexibility for different platforms
- Content centered properly
- No stretching or distortion

## Ratio Quick Reference

```
21:9  ████████████████████████████████  (ultra-wide, cinematic)
16:9  ████████████████████████          (widescreen)
3:1   ████████████████████████████████  (Twitter header)
2:1   ████████████████                  (GitHub README)
4:1   ████████████████████████████████████████████  (LinkedIn)
```

## File Size Guidelines

- **GitHub**: < 10MB, PNG or JPG
- **Twitter**: < 5MB, PNG, JPG, or GIF
- **LinkedIn**: < 8MB, PNG or JPG
- **Website**: Optimize for web (< 500KB ideally)

## Tips

1. **Safe zones**: Keep important content (text, logo) in center 60% for platforms that crop on mobile
2. **Text legibility**: Use high contrast, avoid small text
3. **Brand consistency**: Match colors and style with existing logo
4. **Mobile preview**: Check how banner looks on mobile (often cropped)
scripts/crop_banner.py Script
#!/usr/bin/env python3
"""
Crop banner to target aspect ratio.

Usage:
    python crop_banner.py input.png output.png --ratio 2:1 --width 1280
    python crop_banner.py input.png output.png --ratio 16:9
    python crop_banner.py input.png output.png --size 1500x500
"""

import argparse
import sys
from PIL import Image


def parse_ratio(ratio_str: str) -> tuple[int, int]:
    """Parse ratio string like '2:1' or '16:9' into (width, height) parts."""
    parts = ratio_str.split(':')
    if len(parts) != 2:
        raise ValueError(f"Invalid ratio format: {ratio_str}. Use format like '2:1' or '16:9'")
    return int(parts[0]), int(parts[1])


def parse_size(size_str: str) -> tuple[int, int]:
    """Parse size string like '1280x640' into (width, height)."""
    parts = size_str.lower().split('x')
    if len(parts) != 2:
        raise ValueError(f"Invalid size format: {size_str}. Use format like '1280x640'")
    return int(parts[0]), int(parts[1])


def crop_to_ratio(img: Image.Image, target_ratio: tuple[int, int]) -> Image.Image:
    """Crop image to target aspect ratio, centered."""
    orig_width, orig_height = img.size
    target_w, target_h = target_ratio
    
    target_aspect = target_w / target_h
    orig_aspect = orig_width / orig_height
    
    if orig_aspect > target_aspect:
        # Image is wider than target, crop width
        new_width = int(orig_height * target_aspect)
        new_height = orig_height
        left = (orig_width - new_width) // 2
        top = 0
    else:
        # Image is taller than target, crop height
        new_width = orig_width
        new_height = int(orig_width / target_aspect)
        left = 0
        top = (orig_height - new_height) // 2
    
    right = left + new_width
    bottom = top + new_height
    
    return img.crop((left, top, right, bottom))


def main():
    parser = argparse.ArgumentParser(description='Crop banner to target aspect ratio')
    parser.add_argument('input', help='Input image path')
    parser.add_argument('output', help='Output image path')
    parser.add_argument('--ratio', '-r', help='Target aspect ratio (e.g., 2:1, 16:9)')
    parser.add_argument('--size', '-s', help='Target size (e.g., 1280x640)')
    parser.add_argument('--width', '-w', type=int, help='Target width (maintains ratio)')
    parser.add_argument('--height', type=int, help='Target height (maintains ratio)')
    
    args = parser.parse_args()
    
    if not args.ratio and not args.size:
        print("Error: Must specify --ratio or --size", file=sys.stderr)
        sys.exit(1)
    
    try:
        img = Image.open(args.input)
        print(f"Input: {img.size[0]}x{img.size[1]}")
        
        if args.size:
            target_w, target_h = parse_size(args.size)
            target_ratio = (target_w, target_h)
        else:
            target_ratio = parse_ratio(args.ratio)
            target_w, target_h = None, None
        
        # Crop to ratio
        cropped = crop_to_ratio(img, target_ratio)
        print(f"Cropped: {cropped.size[0]}x{cropped.size[1]}")
        
        # Resize if target dimensions specified
        if args.size:
            cropped = cropped.resize((target_w, target_h), Image.Resampling.LANCZOS)
            print(f"Resized: {cropped.size[0]}x{cropped.size[1]}")
        elif args.width:
            ratio = args.width / cropped.size[0]
            new_height = int(cropped.size[1] * ratio)
            cropped = cropped.resize((args.width, new_height), Image.Resampling.LANCZOS)
            print(f"Resized: {cropped.size[0]}x{cropped.size[1]}")
        elif args.height:
            ratio = args.height / cropped.size[1]
            new_width = int(cropped.size[0] * ratio)
            cropped = cropped.resize((new_width, args.height), Image.Resampling.LANCZOS)
            print(f"Resized: {cropped.size[0]}x{cropped.size[1]}")
        
        cropped.save(args.output)
        print(f"Saved: {args.output}")
        
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()
templates/preview.html Reference
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Banner Preview</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: #1a1a1a;
      color: #fff;
      padding: 20px;
    }
    h1 {
      text-align: center;
      margin-bottom: 20px;
      font-weight: 300;
    }
    .grid {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 16px;
      max-width: 1600px;
      margin: 0 auto;
    }
    .card {
      background: #2a2a2a;
      border-radius: 8px;
      overflow: hidden;
      cursor: pointer;
      transition: transform 0.2s, box-shadow 0.2s;
    }
    .card:hover {
      transform: translateY(-4px);
      box-shadow: 0 8px 24px rgba(0,0,0,0.4);
    }
    .card img {
      width: 100%;
      height: auto;
      display: block;
    }
    .card-info {
      padding: 12px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .card-number {
      font-size: 14px;
      font-weight: 600;
      color: #888;
    }
    .card-fav {
      background: none;
      border: none;
      font-size: 20px;
      cursor: pointer;
      opacity: 0.3;
      transition: opacity 0.2s;
    }
    .card-fav:hover, .card-fav.active {
      opacity: 1;
    }
    
    /* Modal */
    .modal {
      display: none;
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0,0,0,0.95);
      z-index: 1000;
      justify-content: center;
      align-items: center;
      padding: 40px;
    }
    .modal.active { display: flex; }
    .modal img {
      max-width: 100%;
      max-height: 100%;
      object-fit: contain;
    }
    .modal-close {
      position: absolute;
      top: 20px;
      right: 30px;
      font-size: 40px;
      color: #fff;
      cursor: pointer;
      opacity: 0.7;
    }
    .modal-close:hover { opacity: 1; }
    .modal-nav {
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
      font-size: 60px;
      color: #fff;
      cursor: pointer;
      opacity: 0.5;
      user-select: none;
    }
    .modal-nav:hover { opacity: 1; }
    .modal-prev { left: 20px; }
    .modal-next { right: 20px; }
    .modal-info {
      position: absolute;
      bottom: 20px;
      left: 50%;
      transform: translateX(-50%);
      background: rgba(0,0,0,0.8);
      padding: 10px 20px;
      border-radius: 8px;
      font-size: 14px;
    }
    
    /* Favorites summary */
    .favorites {
      position: fixed;
      bottom: 20px;
      right: 20px;
      background: #333;
      padding: 16px 20px;
      border-radius: 8px;
      max-width: 300px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.3);
    }
    .favorites h3 {
      font-size: 14px;
      margin-bottom: 8px;
      color: #888;
    }
    .favorites-list {
      font-size: 16px;
      font-weight: 600;
    }
    .favorites:empty { display: none; }
    
    @media (max-width: 1200px) {
      .grid { grid-template-columns: repeat(2, 1fr); }
    }
    @media (max-width: 768px) {
      .grid { grid-template-columns: 1fr; }
    }
  </style>
</head>
<body>
  <h1>Banner Variations</h1>
  
  <div class="grid" id="grid"></div>
  
  <div class="modal" id="modal">
    <span class="modal-close" onclick="closeModal()">&times;</span>
    <span class="modal-nav modal-prev" onclick="navModal(-1)">&#8249;</span>
    <img id="modal-img" src="" alt="">
    <span class="modal-nav modal-next" onclick="navModal(1)">&#8250;</span>
    <div class="modal-info" id="modal-info"></div>
  </div>
  
  <div class="favorites" id="favorites"></div>

  <script>
    // Configuration - UPDATE THIS
    const BANNER_COUNT = 20;
    const BANNER_PREFIX = 'banner-';
    const BANNER_EXT = '.png';
    
    let currentIndex = 0;
    let favorites = new Set();
    
    function init() {
      const grid = document.getElementById('grid');
      
      for (let i = 1; i <= BANNER_COUNT; i++) {
        const num = String(i).padStart(2, '0');
        const filename = `${BANNER_PREFIX}${num}${BANNER_EXT}`;
        
        const card = document.createElement('div');
        card.className = 'card';
        card.innerHTML = `
          <img src="${filename}" alt="Banner ${num}" onclick="openModal(${i-1})">
          <div class="card-info">
            <span class="card-number">#${num}</span>
            <button class="card-fav" onclick="toggleFav(${i}, event)">★</button>
          </div>
        `;
        grid.appendChild(card);
      }
    }
    
    function openModal(index) {
      currentIndex = index;
      updateModal();
      document.getElementById('modal').classList.add('active');
      document.body.style.overflow = 'hidden';
    }
    
    function closeModal() {
      document.getElementById('modal').classList.remove('active');
      document.body.style.overflow = '';
    }
    
    function navModal(dir) {
      currentIndex = (currentIndex + dir + BANNER_COUNT) % BANNER_COUNT;
      updateModal();
    }
    
    function updateModal() {
      const num = String(currentIndex + 1).padStart(2, '0');
      const filename = `${BANNER_PREFIX}${num}${BANNER_EXT}`;
      document.getElementById('modal-img').src = filename;
      document.getElementById('modal-info').textContent = `Banner #${num}`;
    }
    
    function toggleFav(num, event) {
      event.stopPropagation();
      const btn = event.target;
      if (favorites.has(num)) {
        favorites.delete(num);
        btn.classList.remove('active');
      } else {
        favorites.add(num);
        btn.classList.add('active');
      }
      updateFavorites();
    }
    
    function updateFavorites() {
      const el = document.getElementById('favorites');
      if (favorites.size === 0) {
        el.innerHTML = '';
      } else {
        const nums = Array.from(favorites).sort((a,b) => a-b).map(n => `#${String(n).padStart(2,'0')}`);
        el.innerHTML = `<h3>Favorites</h3><div class="favorites-list">${nums.join(', ')}</div>`;
      }
    }
    
    document.addEventListener('keydown', (e) => {
      if (!document.getElementById('modal').classList.contains('active')) return;
      if (e.key === 'Escape') closeModal();
      if (e.key === 'ArrowLeft') navModal(-1);
      if (e.key === 'ArrowRight') navModal(1);
    });
    
    init();
  </script>
</body>
</html>

Version History

v1.0.0 Imported from GitHub
1 week ago