Lompat ke konten
Rahman FakhruDiskusi proyek

Skills · Claude Skills

Asset Starter — Generate Brand Asset Kit

Skill untuk menghasilkan brand asset kit lengkap dari satu logo PNG: SVG variants, PWA icons, favicon, wordmark, WEBP, dan banners.

On this page · 10

#SKILL.md siap pakai

---
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

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:

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

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:

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:

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)

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)

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.

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:

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

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):

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:

{
  "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:

<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" />

---

Sumber pembelajaran: [Generate semua brand asset dari satu logo](/classroom/claude-skills/generate-asset-claude). Resource ini adalah SSOT artifact; konteks penggunaan tetap dijelaskan di Classroom.