---
name: asset-starter
description: >
  Generate a complete brand asset kit from a single logo mark PNG. Use this skill whenever
  the user uploads a logo image and wants to export assets for a website, app, or PWA —
  including SVG variants (fill, outline, dark/light), PNG icons (192, 512, maskable,
  apple-touch), favicon.ico, WEBP, wordmark (mark + brand name), and horizontal/square
  banners. Trigger this skill when the user mentions any of: "generate assets", "logo kit",
  "SVG variants", "favicon", "PWA icons", "apple touch icon", "maskable icon", "banner",
  "wordmark", "brand assets", or uploads a logo and asks for multiple export formats.
  Also trigger when the user says "asset-starter" or "/asset-starter".
---

# Asset Starter

Generate a full brand asset kit from one logo mark PNG. No external services needed —
runs entirely on PIL + OpenCV in the container.

## What this skill produces

### SVG (7 files)
- `logo-mark-fill-black.svg` — mark filled `#0D0D0D`, transparent bg
- `logo-mark-fill-white.svg` — mark filled `#FFFFFF`, transparent bg
- `logo-mark-outline-black.svg` — stroke only, `#0D0D0D`
- `logo-mark-outline-white.svg` — stroke only, `#FFFFFF`
- `logo-wordmark-light.svg` — mark + brand name, dark-on-transparent
- `logo-wordmark-dark.svg` — mark + brand name, white-on-transparent
- `favicon.svg` — same as fill-black, named for `<link rel="icon">`
### PNG / ICO (5 files)
- `icon-192.png` — 192×192, transparent bg (PWA manifest)
- `icon-512.png` — 512×512, transparent bg (PWA manifest)
- `maskable-icon-512.png` — 512×512, white bg, 18% safe-zone padding
- `apple-touch-icon.png` — 180×180, white bg
- `favicon.ico` — multi-resolution: 16, 32, 48, 64, 128, 256 px
### WEBP (3 files)
- `icon-192.webp`, `icon-512.webp`, `maskable-icon-512.webp`
### Wordmark PNG + WEBP (4 files)
- `logo-wordmark-light.png/.webp` — 1200px wide, transparent bg
- `logo-wordmark-dark.png/.webp` — 1200px wide, dark `#1A1A1A` bg
### Banner PNG + WEBP + SVG (9 files)
- `banner-dark.png/.webp/.svg` — 1920×640, black bg, white content
- `banner-light.png/.webp/.svg` — 1920×640, white bg, dark content
- `banner-sq-dark.png/.webp` — 1080×1080, square, black bg
**Total: ~28 files**

---

## Step 0 — Gather requirements

Before generating anything, confirm these details with the user. Ask in a single message — do NOT generate assets until all answers are received.

**Required questions:**

1. **Logo mark PNG** — already uploaded? If not, ask for it.
2. **Logo background** — is the logo on a **dark/black** bg (use `THRESH_BINARY`) or **light/white** bg (use `THRESH_BINARY_INV`)? Claude can usually detect this from the image.
3. **Brand name** — what text goes in wordmarks and banners? (e.g. "Rahman Resource")
4. **Tagline / description** — short line shown below brand name in banners. (e.g. "free resource for nextjs and convex projects only"). If none, banners will be brand-name only.
5. **Mark size in icons** — default is large (pad 5%). Ask only if user seems to want something specific.
6. **Wordmark font style** — default: Poppins Light. Ask only if user mentions it.
**Shortcut:** If the user already provided the PNG and answered most questions in their message, extract what you can and ask only for what's missing (max 1–2 follow-up questions).

**Banner behavior:** Banners always show `BRAND + TAGLINE` in a safe two-line layout that auto-wraps and never clips. Pure mark files (SVG marks, icons, favicon) never include text.

---

## Step 1 — Build RGBA masks from source PNG

```python
import cv2, numpy as np
from PIL import Image

src_pil = Image.open(SRC).convert("RGB")
src_np  = np.array(src_pil)
gray    = cv2.cvtColor(src_np, cv2.COLOR_RGB2GRAY)
_, mask = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY_INV)
H, W    = mask.shape

def make_rgba(r, g, b, alpha_mask):
    arr = np.zeros((H, W, 4), dtype=np.uint8)
    arr[:,:,0]=r; arr[:,:,1]=g; arr[:,:,2]=b; arr[:,:,3]=alpha_mask
    return Image.fromarray(arr, "RGBA")

logo_black = make_rgba(13,  13,  13,  mask)
logo_white = make_rgba(255, 255, 255, mask)
```

**Threshold rule:** `< 200` = logo pixel, `>= 200` = background. Works for logos on
white or near-white backgrounds. If the logo is on a dark background, invert: use
`cv2.THRESH_BINARY` instead of `cv2.THRESH_BINARY_INV`.

---

## Step 2 — Trace SVG path via OpenCV contours

```python
contours, * = cv2.findContours(mask, cv2.RETR*EXTERNAL, cv2.CHAIN_APPROX_NONE)
contours    = sorted(contours, key=cv2.contourArea, reverse=True)
main_cnt    = contours[0]
approx      = cv2.approxPolyDP(main_cnt, 1.5, True).reshape(-1, 2).tolist()

d_path  = "M " + " L ".join(f"{x},{y}" for x, y in approx) + " Z"
viewBox = f"0 0 {W} {H}"
SW      = max(W, H) * 0.012  # stroke width for outline variants
```

**epsilon=1.5** gives ~80–120 anchor points — enough fidelity without bloating file size.
For logos with very thin strokes or sharp serifs, lower to 0.8. For simple geometric
marks, raise to 3.0.

If the logo has **multiple disconnected shapes** (e.g. an icon + separate dot), use
`cv2.RETR_CCOMP` and loop over all contours above a minimum area threshold:
```python
contours, hier = cv2.findContours(mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
path_ds = [contour_to_d(c) for c in contours if cv2.contourArea(c) > 500]
```

---

## Step 3 — SVG builders

```python
def build_p(fill="#000000", stroke=None, sw=SW):
    fa = f'fill="{fill}" fill-rule="evenodd"'
    sa = (f'stroke="{stroke}" stroke-width="{sw:.1f}" '
          f'stroke-linejoin="round" stroke-linecap="round"') if stroke else ""
    return f'    <path {fa} {sa} d="{d_path}"/>'

def mark_svg(fill, stroke=None, sw=SW, bg=None):
    bg_el = f'  <rect width="100%" height="100%" fill="{bg}"/>' if bg else ""
    return (f'<?xml version="1.0" encoding="UTF-8"?>\n'
            f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{viewBox}">\n'
            f'{bg_el}\n{build_p(fill, stroke, sw)}\n</svg>')
```

**Wordmark SVG** — mark stacked above brand name text:
```python
TEXT_LABEL = "Brand Name"       # ← replace with actual brand name
TEXT_SIZE  = H * 0.11
LETTER_SPC = W * 0.01
TOTAL_H    = H + H*0.07 + TEXT_SIZE * 1.3

def wordmark_svg(mark_fill, text_fill, bg=None):
    bg_el  = f'  <rect width="100%" height="100%" fill="{bg}"/>' if bg else ""
    text_y = H + H*0.07 + TEXT_SIZE
    return (
        f'<?xml version="1.0" encoding="UTF-8"?>\n'
        f'<svg xmlns="http://www.w3.org/2000/svg" '
        f'viewBox="0 0 {W:.0f} {TOTAL_H:.0f}">\n'
        f'{bg_el}\n{build_p(mark_fill)}\n'
        f'  <text x="{W/2:.0f}" y="{text_y:.0f}"\n'
        f'    font-family="\'Poppins\', \'Helvetica Neue\', Arial, sans-serif"\n'
        f'    font-size="{TEXT_SIZE:.0f}" font-weight="300"\n'
        f'    letter-spacing="{LETTER_SPC:.1f}" text-anchor="middle"\n'
        f'    fill="{text_fill}">{TEXT_LABEL}</text>\n'
        f'</svg>')
```

**Banner SVG** — horizontal layout, mark-left + text-right:
```python
def banner_svg(mark_fill, text_fill, bg, bw=1920, bh=640):
    scale    = int(bh * 0.55) / max(W, H)
    sw, sh   = W * scale, H * scale
    left_pad = int(bw * 0.075)
    mark_y   = (bh - sh) / 2
    text_x   = left_pad + sw + int(bw * 0.04)
    fnt_sz   = bh * 0.225
    text_y   = bh / 2 + fnt_sz * 0.35
    transform = f'translate({left_pad},{mark_y:.1f}) scale({scale:.4f})'
    bg_el    = f'<rect width="{bw}" height="{bh}" fill="{bg}"/>'
    return (
        f'<?xml version="1.0" encoding="UTF-8"?>\n'
        f'<svg xmlns="http://www.w3.org/2000/svg" '
        f'viewBox="0 0 {bw} {bh}">\n'
        f'  {bg_el}\n'
        f'  <g transform="{transform}">{build_p(mark_fill)}</g>\n'
        f'  <text x="{text_x:.1f}" y="{text_y:.1f}"\n'
        f'    font-family="\'Poppins\', \'Helvetica Neue\', Arial, sans-serif"\n'
        f'    font-size="{fnt_sz:.1f}" font-weight="300"\n'
        f'    fill="{text_fill}">{TEXT_LABEL}</text>\n'
        f'</svg>')
```

---

## Step 4 — Raster PNG helpers (PIL only, no cairosvg)

```python
def resize_logo(logo_img, size, pad_ratio=0.12, bg=None):
    inner   = int(size * (1 - 2 * pad_ratio))
    lcp     = logo_img.copy()
    lcp.thumbnail((inner, inner), Image.LANCZOS)
    rw, rh  = lcp.size
    canvas  = Image.new("RGBA", (size, size), bg if bg else (0,0,0,0))
    canvas.paste(lcp, ((size-rw)//2, (size-rh)//2), lcp)
    return canvas
```

**Padding rules:**
| Asset                | `pad_ratio` | Background       |
|----------------------|-------------|------------------|
| icon-192, icon-512   | 0.05        | transparent      |
| maskable-icon-512    | 0.18        | `#FFFFFF`        |
| apple-touch-icon     | 0.05        | `#FFFFFF`        |
| favicon.ico base     | 0.05        | `#FFFFFF`        |

---

## Step 5 — Wordmark raster (PIL with Poppins)

```python
from PIL import ImageFont, ImageDraw

FONT_LIGHT = "/usr/share/fonts/truetype/google-fonts/Poppins-Light.ttf"

def make_wordmark_pil(logo_img, text_color, bg_color=None, width=1200):
    mark_w = int(width * 0.38)
    lcp    = logo_img.copy()
    lcp.thumbnail((mark_w, mark_w), Image.LANCZOS)
    mw, mh = lcp.size
    fnt_sz = int(width * 0.057)
    fnt    = ImageFont.truetype(FONT_LIGHT, fnt_sz)
    gap    = int(mh * 0.10)
    canvas = Image.new("RGBA", (width, mh + gap + int(fnt_sz*1.35)),
                        bg_color if bg_color else (0,0,0,0))
    canvas.paste(lcp, ((width-mw)//2, 0), lcp)
    draw = ImageDraw.Draw(canvas)
    bb   = draw.textbbox((0,0), TEXT_LABEL, font=fnt)
    tx   = (width - (bb[2]-bb[0])) // 2
    draw.text((tx, mh+gap), TEXT_LABEL, font=fnt, fill=text_color)
    return canvas
```

---

## Step 6 — Banner raster (PIL) — safe two-line layout

Banners use a **two-line text layout** (brand name + tagline) that auto-sizes fonts and wraps the tagline to prevent clipping. Never hardcode font sizes — always compute them dynamically.

```python
def get_font(size):
    for path in [
        "/usr/share/fonts/truetype/google-fonts/Poppins-Light.ttf",
        "/usr/share/fonts/truetype/google-fonts/Poppins-Regular.ttf",
        "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
    ]:
        try: return ImageFont.truetype(path, size)
        except: pass
    return ImageFont.load_default()

def get_font_bold(size):
    for path in [
        "/usr/share/fonts/truetype/google-fonts/Poppins-SemiBold.ttf",
        "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
    ]:
        try: return ImageFont.truetype(path, size)
        except: pass
    return get_font(size)

def make_banner(logo_img, text_color, tagline_color, bg_color, width=1920, height=640):
    canvas   = Image.new("RGBA", (width, height), bg_color)
    mark_max = int(height * 0.52)
    lcp = logo_img.copy()
    lcp.thumbnail((mark_max, mark_max), Image.LANCZOS)
    mw, mh   = lcp.size
    left_pad = int(width * 0.07)
    canvas.paste(lcp, (left_pad, (height-mh)//2), lcp)

    text_x  = left_pad + mw + int(width * 0.04)
    avail_w = int(width * 0.94) - text_x
    draw    = ImageDraw.Draw(canvas)

    # Brand name: shrink until it fits avail_w
    brand_sz = int(height * 0.20)
    while brand_sz > 20:
        fnt = get_font_bold(brand_sz)
        bb  = draw.textbbox((0,0), BRAND, font=fnt)
        if (bb[2]-bb[0]) <= avail_w: break
        brand_sz -= 4
    fnt_brand = get_font_bold(brand_sz)

    # Tagline: 42% of brand size, word-wrapped
    tag_sz  = max(int(brand_sz * 0.42), 18)
    fnt_tag = get_font(tag_sz)
    words   = TAGLINE.split()
    lines, line = [], ""
    for w in words:
        test = (line + " " + w).strip()
        bb   = draw.textbbox((0,0), test, font=fnt_tag)
        if (bb[2]-bb[0]) <= avail_w: line = test
        else:
            if line: lines.append(line)
            line = w
    if line: lines.append(line)

    # Measure and vertically center text block
    bb_b   = draw.textbbox((0,0), BRAND, font=fnt_brand)
    bh     = bb_b[3] - bb_b[1]
    bb_t   = draw.textbbox((0,0), lines[0] if lines else "", font=fnt_tag)
    th     = bb_t[3] - bb_t[1]
    gap_bt = int(brand_sz * 0.25)
    gap_tl = int(tag_sz * 0.30)
    n      = len(lines)
    total_h = bh + (gap_bt + n*th + (n-1)*gap_tl if TAGLINE else 0)
    ty      = (height - total_h) // 2

    draw.text((text_x, ty), BRAND, font=fnt_brand, fill=text_color)
    if TAGLINE:
        y = ty + bh + gap_bt
        for ln in lines:
            draw.text((text_x, y), ln, font=fnt_tag, fill=tagline_color)
            y += th + gap_tl
    return canvas
```

Produce three variants:
```python
banner_dark    = make_banner(logo_white, (255,255,255,255), (180,180,180,255), (10,10,10,255),   1920, 640)
banner_light   = make_banner(logo_black, (13,13,13,255),   (80,80,80,255),    (255,255,255,255), 1920, 640)
banner_sq_dark = make_banner(logo_white, (255,255,255,255), (180,180,180,255), (10,10,10,255),   1080, 1080)
```

---

## Step 7 — Save all files

```python
import os
OUT = "/mnt/user-data/outputs"

# SVG
for name, content in svg_defs.items():
    with open(os.path.join(OUT, name), "w") as f: f.write(content)

# PNG
icon192.save(os.path.join(OUT, "icon-192.png"), "PNG")
icon512.save(os.path.join(OUT, "icon-512.png"), "PNG")
mask512.save(os.path.join(OUT, "maskable-icon-512.png"), "PNG")
apple.save(os.path.join(OUT, "apple-touch-icon.png"), "PNG")

# ICO (multi-resolution)
ico_base.save(os.path.join(OUT, "favicon.ico"), format="ICO",
              sizes=[(16,16),(32,32),(48,48),(64,64),(128,128),(256,256)])

# WEBP
icon192.save(os.path.join(OUT, "icon-192.webp"), "WEBP", quality=90)
icon512.save(os.path.join(OUT, "icon-512.webp"), "WEBP", quality=90)
mask512.save(os.path.join(OUT, "maskable-icon-512.webp"), "WEBP", quality=90)

# Wordmark
wm_light.save(os.path.join(OUT, "logo-wordmark-light.png"), "PNG")
wm_dark.save(os.path.join(OUT, "logo-wordmark-dark.png"), "PNG")
wm_light.save(os.path.join(OUT, "logo-wordmark-light.webp"), "WEBP", quality=92)
wm_dark.save(os.path.join(OUT, "logo-wordmark-dark.webp"), "WEBP", quality=92)

# Banners
for img, name, fmt, kw in [
    (banner_dark,    "banner-dark.png",     "PNG",  {}),
    (banner_dark,    "banner-dark.webp",    "WEBP", {"quality":92}),
    (banner_light,   "banner-light.png",    "PNG",  {}),
    (banner_light,   "banner-light.webp",   "WEBP", {"quality":92}),
    (banner_sq_dark, "banner-sq-dark.png",  "PNG",  {}),
    (banner_sq_dark, "banner-sq-dark.webp", "WEBP", {"quality":92}),
]:
    img.save(os.path.join(OUT, name), fmt, **kw)
```

---

## Step 8 — Present files

After all files are saved, call `present_files` **once** with ALL output files in a single call — never split into multiple batches. Order them for visual impact:

```
banners (PNG first, then WEBP, then SVG)
→ wordmarks (PNG, WEBP, SVG)
→ SVG marks (fill-white, fill-black, outline-black, outline-white, favicon.svg)
→ PNG icons (icon-512, icon-192, maskable-icon-512, apple-touch-icon)
→ WEBP icons (icon-512, icon-192, maskable-icon-512)
→ favicon.ico
```

Example (all paths listed in one `present_files` call):
```python
present_files([
    OUT/banner-dark.png, OUT/banner-light.png, OUT/banner-sq-dark.png,
    OUT/banner-dark.webp, OUT/banner-light.webp, OUT/banner-sq-dark.webp,
    OUT/banner-dark.svg, OUT/banner-light.svg,
    OUT/logo-wordmark-dark.png, OUT/logo-wordmark-light.png,
    OUT/logo-wordmark-dark.webp, OUT/logo-wordmark-light.webp,
    OUT/logo-wordmark-dark.svg, OUT/logo-wordmark-light.svg,
    OUT/logo-mark-fill-white.svg, OUT/logo-mark-fill-black.svg,
    OUT/logo-mark-outline-black.svg, OUT/logo-mark-outline-white.svg,
    OUT/favicon.svg,
    OUT/icon-512.png, OUT/icon-192.png,
    OUT/maskable-icon-512.png, OUT/apple-touch-icon.png,
    OUT/icon-512.webp, OUT/icon-192.webp, OUT/maskable-icon-512.webp,
    OUT/favicon.ico,
])
```

Then print a summary table of all generated files with sizes.

---

## Edge cases & troubleshooting

| Symptom | Fix |
|---|---|
| Mask captures noise/artifacts | Raise threshold: `cv2.threshold(gray, 230, ...)` |
| Logo on dark/black background | Switch to `cv2.THRESH_BINARY` (not `_INV`) |
| SVG path looks jagged | Lower epsilon: `cv2.approxPolyDP(cnt, 0.8, ...)` |
| SVG path too blocky/simplified | Lower epsilon: try `0.51.0` |
| Multiple disconnected shapes | Use `cv2.RETR_CCOMP` and loop all contours |
| Wordmark text clipped/too large | Adjust `fnt_sz = int(width * 0.04)` (lower = smaller) |
| Font not found | Fallback: `ImageFont.truetype("/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", fnt_sz)` |
| Banner text overflows right edge | Use dynamic sizing loop: shrink `brand_sz` until `textbbox width <= avail_w` |
| Tagline wraps badly / overflows  | Use word-wrap loop with `avail_w` guard — never hardcode tagline font size |
| No tagline provided              | Set `TAGLINE = ""` and skip tagline drawing block entirely |

---

## manifest.json snippet (Next.js / PWA)

After generating, offer this snippet to the user:

```json
{
  "icons": [
    { "src": "/icon-192.png",         "sizes": "192x192", "type": "image/png" },
    { "src": "/icon-512.png",         "sizes": "512x512", "type": "image/png" },
    { "src": "/maskable-icon-512.png","sizes": "512x512", "type": "image/png", "purpose": "maskable" }
  ]
}
```

And the `<head>` tags:
```html
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
```
