Resource
Resource untuk pelajaran ini
Gunakan resource ini bersama materi. Update resource akan otomatis tercermin tanpa menyalin isi ke pelajaran.
asset-starter.skill Unduh .skill
---
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" />
```