feat: rebrand to website design, Apple HIG icons, ASCII onboarding animation
Build & Release / Lint & Typecheck (push) Successful in 38s
Build & Release / Unit Tests (push) Successful in 31s
Build & Release / Windows (arm64) (push) Failing after 11s
Build & Release / Windows (x64) (push) Failing after 27s
Build & Release / Linux (arm64) (push) Canceled after 0s
Build & Release / Linux (x64) (push) Canceled after 0s
Build & Release / Publish Release (push) Canceled after 0s
Build & Release / macOS (arm64) (push) Canceled after 0s
Build & Release / macOS (x64) (push) Canceled after 0s
Build & Release / Lint & Typecheck (push) Successful in 38s
Build & Release / Unit Tests (push) Successful in 31s
Build & Release / Windows (arm64) (push) Failing after 11s
Build & Release / Windows (x64) (push) Failing after 27s
Build & Release / Linux (arm64) (push) Canceled after 0s
Build & Release / Linux (x64) (push) Canceled after 0s
Build & Release / Publish Release (push) Canceled after 0s
Build & Release / macOS (arm64) (push) Canceled after 0s
Build & Release / macOS (x64) (push) Canceled after 0s
- Align Electron CSS with website design schema (Inter font, #FBFAF5/#191919 palette, #213448/#94B4C1 accent) - Generate proper app icons from logo.svg via cairosvg (no more Pillow ladder) - Square bg icons for Electron/tray (Apple HIG — OS applies squircle mask) - Circle bg icons for website favicon only - Add scrolling ASCII agent-activity animation to onboarding hero left panel - Remove dock.setIcon() — let macOS use bundle .icns with system mask - Fix goal title overflow with min-w-0 for flex truncation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
273244817a
commit
eda0e26adb
18 files changed
+337
-237
No files matched your search
+142
-169
@@ -1,222 +1,195 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate AgentCompany app icons (PNG, ICO, ICNS) and tray icons.
|
||||
"""Generate AgentCompany icons from SVG sources using cairosvg.
|
||||
|
||||
Design v4: Circle background #191919 with #FBFAF5 "A" logo.
|
||||
Uses the actual logo paths from logo.svg rendered via cairosvg/Pillow.
|
||||
Two icon families:
|
||||
- Web (circle bg): icon.svg → landing favicon + website logo
|
||||
- App (square bg): icon-app.svg → Electron app icon (Apple HIG, OS applies mask)
|
||||
- Tray: rendered from logo.svg for menu bar / notification area
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
# ── Ensure cairo library is discoverable on macOS (Homebrew) ──────────
|
||||
for _lib_dir in ("/opt/homebrew/opt/cairo/lib", "/usr/local/opt/cairo/lib"):
|
||||
if Path(_lib_dir).exists():
|
||||
os.environ["DYLD_LIBRARY_PATH"] = (
|
||||
_lib_dir + ":" + os.environ.get("DYLD_LIBRARY_PATH", "")
|
||||
)
|
||||
break
|
||||
|
||||
import cairosvg # noqa: E402 — must come after DYLD_LIBRARY_PATH setup
|
||||
from PIL import Image # noqa: E402
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ASSETS = ROOT / "assets"
|
||||
LANDING = ROOT / "landing" / "public"
|
||||
ICONSET = ASSETS / "icon.iconset"
|
||||
PNG_PATH = ASSETS / "icon.png"
|
||||
ICO_PATH = ASSETS / "icon.ico"
|
||||
ICNS_PATH = ASSETS / "icon.icns"
|
||||
SVG_PATH = ASSETS / "icon.svg"
|
||||
LOGO_SVG = ROOT / "logo.svg"
|
||||
|
||||
# SVG sources
|
||||
WEB_SVG = ASSETS / "icon.svg" # circle bg — web favicon
|
||||
APP_SVG = ASSETS / "icon-app.svg" # square bg — Electron (Apple HIG)
|
||||
|
||||
SIZE = 1024
|
||||
|
||||
# ── Brand palette ──────────────────────────────────────────────────────
|
||||
BG_COLOR = (25, 25, 25) # #191919
|
||||
LOGO_COLOR = (251, 250, 245) # #FBFAF5
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
def build_icon() -> Image.Image:
|
||||
"""Build the app icon by rendering icon.svg via cairosvg, or fallback to Pillow."""
|
||||
# Try cairosvg first (best quality)
|
||||
try:
|
||||
import cairosvg
|
||||
svg_data = SVG_PATH.read_bytes()
|
||||
png_data = cairosvg.svg2png(bytestring=svg_data, output_width=SIZE, output_height=SIZE)
|
||||
import io
|
||||
return Image.open(io.BytesIO(png_data)).convert("RGBA")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback: draw programmatically with Pillow
|
||||
icon = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(icon)
|
||||
|
||||
# Circle background
|
||||
margin = SIZE // 2 - 480
|
||||
draw.ellipse(
|
||||
(margin, margin, SIZE - margin, SIZE - margin),
|
||||
fill=(*BG_COLOR, 255),
|
||||
def svg_to_image(svg_path: Path, width: int, height: int | None = None) -> Image.Image:
|
||||
"""Render an SVG to a Pillow RGBA Image at the given size."""
|
||||
h = height or width
|
||||
png_data = cairosvg.svg2png(
|
||||
url=str(svg_path), output_width=width, output_height=h,
|
||||
)
|
||||
return Image.open(io.BytesIO(png_data)).convert("RGBA")
|
||||
|
||||
# Draw the "A" monogram — same geometry as the old icon but in new color
|
||||
cx = SIZE // 2
|
||||
apex_y = 200
|
||||
base_y = 800
|
||||
leg_w = 64
|
||||
base_half = 250
|
||||
fill = (*LOGO_COLOR, 245)
|
||||
|
||||
def leg_x(y: int) -> tuple[float, float]:
|
||||
t = (y - apex_y) / (base_y - apex_y)
|
||||
spread = base_half * t
|
||||
return (cx - spread, cx + spread)
|
||||
|
||||
ll_top, _ = leg_x(apex_y)
|
||||
ll_bot, _ = leg_x(base_y)
|
||||
_, lr_top = leg_x(apex_y)
|
||||
_, lr_bot = leg_x(base_y)
|
||||
|
||||
left_leg = [
|
||||
(ll_top - leg_w * 0.35, apex_y),
|
||||
(ll_top + leg_w * 0.35, apex_y),
|
||||
(ll_bot + leg_w, base_y),
|
||||
(ll_bot - leg_w, base_y),
|
||||
]
|
||||
right_leg = [
|
||||
(lr_top - leg_w * 0.35, apex_y),
|
||||
(lr_top + leg_w * 0.35, apex_y),
|
||||
(lr_bot + leg_w, base_y),
|
||||
(lr_bot - leg_w, base_y),
|
||||
]
|
||||
|
||||
draw.polygon(left_leg, fill=fill)
|
||||
draw.polygon(right_leg, fill=fill)
|
||||
|
||||
# Crossbars — all same cream color with varying opacity for hierarchy
|
||||
bar_h = 42
|
||||
bar_r = 12
|
||||
bar_pad = 14
|
||||
|
||||
tiers = [
|
||||
(440, (*LOGO_COLOR, 255)),
|
||||
(545, (*LOGO_COLOR, 210)),
|
||||
(650, (*LOGO_COLOR, 175)),
|
||||
]
|
||||
|
||||
for bar_cy, bar_fill in tiers:
|
||||
left_cx, right_cx = leg_x(bar_cy)
|
||||
bx0 = int(left_cx + bar_pad)
|
||||
bx1 = int(right_cx - bar_pad)
|
||||
draw.rounded_rectangle(
|
||||
(bx0, bar_cy - bar_h // 2, bx1, bar_cy + bar_h // 2),
|
||||
radius=bar_r,
|
||||
fill=bar_fill,
|
||||
)
|
||||
|
||||
# Apex beacon
|
||||
dot_r = 20
|
||||
dot_cy = apex_y - 2
|
||||
draw.ellipse(
|
||||
(cx - dot_r, dot_cy - dot_r, cx + dot_r, dot_cy + dot_r),
|
||||
fill=(*LOGO_COLOR, 255),
|
||||
def svg_bytes_to_image(svg_bytes: bytes, width: int, height: int | None = None) -> Image.Image:
|
||||
"""Render SVG bytes to a Pillow RGBA Image."""
|
||||
h = height or width
|
||||
png_data = cairosvg.svg2png(
|
||||
bytestring=svg_bytes, output_width=width, output_height=h,
|
||||
)
|
||||
|
||||
return icon
|
||||
return Image.open(io.BytesIO(png_data)).convert("RGBA")
|
||||
|
||||
|
||||
def build_tray_icons() -> None:
|
||||
"""Generate tray/notification area icons for all platforms."""
|
||||
# Color tray icon (Windows/Linux) — 32x32 and 64x64
|
||||
for name, size in [("tray-icon.png", 32), ("tray-icon@2x.png", 64)]:
|
||||
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
margin = 1
|
||||
draw.ellipse((margin, margin, size - margin, size - margin), fill=(*BG_COLOR, 255))
|
||||
# ── Web icons (circle background) ────────────────────────────────────
|
||||
|
||||
# Simple A shape scaled to tray size
|
||||
cx = size // 2
|
||||
top_y = int(size * 0.18)
|
||||
bot_y = int(size * 0.82)
|
||||
spread = int(size * 0.28)
|
||||
w = max(2, int(size * 0.06))
|
||||
def generate_web_icons() -> None:
|
||||
"""Generate favicon.ico and icon.png for the landing site (circle bg)."""
|
||||
print("Generating web icons from icon.svg (circle bg)…")
|
||||
master = svg_to_image(WEB_SVG, SIZE)
|
||||
|
||||
draw.polygon([
|
||||
(cx - w, top_y), (cx + w, top_y),
|
||||
(cx + spread + w, bot_y), (cx + spread - w, bot_y),
|
||||
], fill=(*LOGO_COLOR, 240))
|
||||
draw.polygon([
|
||||
(cx - w, top_y), (cx + w, top_y),
|
||||
(cx - spread + w, bot_y), (cx - spread - w, bot_y),
|
||||
], fill=(*LOGO_COLOR, 240))
|
||||
# PNG for apple-touch-icon / og
|
||||
web_png = LANDING / "icon.png"
|
||||
master.save(web_png, format="PNG")
|
||||
print(f" → {web_png}")
|
||||
|
||||
# Crossbar
|
||||
bar_y = int(size * 0.52)
|
||||
bar_h = max(2, int(size * 0.06))
|
||||
bar_left = cx - int(spread * 0.55)
|
||||
bar_right = cx + int(spread * 0.55)
|
||||
draw.rectangle((bar_left, bar_y, bar_right, bar_y + bar_h), fill=(*LOGO_COLOR, 255))
|
||||
# Copy SVG to landing
|
||||
shutil.copy2(WEB_SVG, LANDING / "icon.svg")
|
||||
print(f" → {LANDING / 'icon.svg'}")
|
||||
|
||||
img.save(ASSETS / name, format="PNG")
|
||||
|
||||
# macOS template icons (monochrome black + alpha)
|
||||
for name, size in [("tray-iconTemplate.png", 22), ("tray-iconTemplate@2x.png", 44)]:
|
||||
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
cx = size // 2
|
||||
top_y = int(size * 0.15)
|
||||
bot_y = int(size * 0.85)
|
||||
spread = int(size * 0.3)
|
||||
w = max(1, int(size * 0.06))
|
||||
|
||||
draw.polygon([
|
||||
(cx - w, top_y), (cx + w, top_y),
|
||||
(cx + spread + w, bot_y), (cx + spread - w, bot_y),
|
||||
], fill=(0, 0, 0, 220))
|
||||
draw.polygon([
|
||||
(cx - w, top_y), (cx + w, top_y),
|
||||
(cx - spread + w, bot_y), (cx - spread - w, bot_y),
|
||||
], fill=(0, 0, 0, 220))
|
||||
|
||||
bar_y = int(size * 0.52)
|
||||
bar_h = max(1, int(size * 0.06))
|
||||
bar_left = cx - int(spread * 0.55)
|
||||
bar_right = cx + int(spread * 0.55)
|
||||
draw.rectangle((bar_left, bar_y, bar_right, bar_y + bar_h), fill=(0, 0, 0, 240))
|
||||
|
||||
img.save(ASSETS / name, format="PNG")
|
||||
|
||||
|
||||
def save_png_and_ico(image: Image.Image) -> None:
|
||||
ASSETS.mkdir(parents=True, exist_ok=True)
|
||||
image.save(PNG_PATH, format="PNG")
|
||||
image.save(
|
||||
ICO_PATH,
|
||||
format="ICO",
|
||||
# ICO with multiple sizes for favicon
|
||||
favicon = LANDING / "favicon.ico"
|
||||
master.save(
|
||||
favicon, format="ICO",
|
||||
sizes=[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
|
||||
)
|
||||
print(f" → {favicon}")
|
||||
|
||||
|
||||
def save_icns(image: Image.Image) -> None:
|
||||
# ── App icons (square background, Apple HIG) ─────────────────────────
|
||||
|
||||
def generate_app_icons() -> None:
|
||||
"""Generate Electron app icons: PNG, ICO, ICNS (square bg, Apple HIG)."""
|
||||
print("Generating app icons from icon-app.svg (square bg, Apple HIG)…")
|
||||
master = svg_to_image(APP_SVG, SIZE)
|
||||
|
||||
# Main PNG
|
||||
png_path = ASSETS / "icon.png"
|
||||
master.save(png_path, format="PNG")
|
||||
print(f" → {png_path}")
|
||||
|
||||
# Windows ICO
|
||||
ico_path = ASSETS / "icon.ico"
|
||||
master.save(
|
||||
ico_path, format="ICO",
|
||||
sizes=[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
|
||||
)
|
||||
print(f" → {ico_path}")
|
||||
|
||||
# macOS ICNS via iconutil
|
||||
generate_icns(master)
|
||||
|
||||
|
||||
def generate_icns(master: Image.Image) -> None:
|
||||
"""Generate .icns from a master image using macOS iconutil."""
|
||||
if ICONSET.exists():
|
||||
shutil.rmtree(ICONSET)
|
||||
ICONSET.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for size in (16, 32, 64, 128, 256, 512):
|
||||
image.resize((size, size), Image.Resampling.LANCZOS).save(
|
||||
master.resize((size, size), Image.Resampling.LANCZOS).save(
|
||||
ICONSET / f"icon_{size}x{size}.png", format="PNG",
|
||||
)
|
||||
image.resize((size * 2, size * 2), Image.Resampling.LANCZOS).save(
|
||||
master.resize((size * 2, size * 2), Image.Resampling.LANCZOS).save(
|
||||
ICONSET / f"icon_{size}x{size}@2x.png", format="PNG",
|
||||
)
|
||||
|
||||
icns_path = ASSETS / "icon.icns"
|
||||
subprocess.run(
|
||||
["iconutil", "-c", "icns", str(ICONSET), "-o", str(ICNS_PATH)],
|
||||
["iconutil", "-c", "icns", str(ICONSET), "-o", str(icns_path)],
|
||||
check=True,
|
||||
)
|
||||
shutil.rmtree(ICONSET)
|
||||
print(f" → {icns_path}")
|
||||
|
||||
|
||||
# ── Tray / notification area icons ───────────────────────────────────
|
||||
|
||||
def generate_tray_icons() -> None:
|
||||
"""Generate tray icons following Apple HIG for menu bar.
|
||||
|
||||
- Color (Windows/Linux): square bg with logo, 32×32 and 64×64
|
||||
- Template (macOS): monochrome black logo on transparent, 22×22 and 44×44
|
||||
"""
|
||||
print("Generating tray icons…")
|
||||
|
||||
# Color tray icons — render app icon (square bg) at small sizes
|
||||
for name, size in [("tray-icon.png", 32), ("tray-icon@2x.png", 64)]:
|
||||
img = svg_to_image(APP_SVG, size)
|
||||
img.save(ASSETS / name, format="PNG")
|
||||
print(f" → {ASSETS / name}")
|
||||
|
||||
# macOS template icons — black logo silhouette on transparent
|
||||
# Read logo.svg paths, render as black on transparent
|
||||
logo_svg = LOGO_SVG.read_text()
|
||||
|
||||
for name, size in [("tray-iconTemplate.png", 22), ("tray-iconTemplate@2x.png", 44)]:
|
||||
# Create SVG with black fill, sized to target
|
||||
template_svg = _make_template_svg(logo_svg, size)
|
||||
img = svg_bytes_to_image(template_svg.encode("utf-8"), size)
|
||||
img.save(ASSETS / name, format="PNG")
|
||||
print(f" → {ASSETS / name}")
|
||||
|
||||
|
||||
def _make_template_svg(logo_svg_content: str, target_size: int) -> str:
|
||||
"""Create a macOS template SVG: black logo silhouette on transparent bg."""
|
||||
# Logo viewBox is 0 0 122 126 — scale to fit target with padding
|
||||
padding = max(2, int(target_size * 0.1))
|
||||
available = target_size - 2 * padding
|
||||
# Scale to fit height (taller dimension)
|
||||
scale = available / 126
|
||||
logo_w = 122 * scale
|
||||
# Center horizontally
|
||||
offset_x = (target_size - logo_w) / 2
|
||||
offset_y = padding
|
||||
|
||||
return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {target_size} {target_size}" fill="none">
|
||||
<g transform="translate({offset_x:.1f} {offset_y:.1f}) scale({scale:.4f})">
|
||||
<path fill="#000000" d="M 76.604813 111.661926 C 65.070183 111.662621 53.987053 111.662621 42.757744 111.662621 C 45.2239 101.998177 45.297546 101.949265 53.830105 102.036659 C 57.279823 102.071999 60.730202 102.04248 64.259201 102.04248 C 62.159458 93.316338 61.799103 93.009819 51.340881 91.341682 C 59.312901 73.847687 67.193756 56.553749 75.587578 38.134155 C 86.933441 62.965954 97.804642 86.758881 108.711754 110.630394 C 100.707413 111.871689 92.355965 107.246559 88.107536 98.852119 C 84.207748 91.146538 80.800591 83.191696 77.172813 75.348343 C 76.340195 73.54818 75.492958 71.754791 74.614624 69.877441 C 69.107384 75.691788 68.008286 81.475128 71.005882 88.088753 C 74.413452 95.606903 77.813919 103.128281 81.452705 111.167984 C 79.55114 111.381325 78.303734 111.521271 76.604813 111.661926 Z"/>
|
||||
<path fill="#000000" d="M 26.805992 87.743256 C 38.616821 61.830872 50.261688 36.260269 62.470131 9.452141 C 65.732315 17.818909 71.145126 23.524567 66.258224 33.086182 C 55.518253 54.099815 46.339973 75.911873 36.533691 97.402336 C 32.699284 105.805443 26.495394 110.823158 16.012421 111.410957 C 19.72117 103.270836 23.180601 95.67794 26.805992 87.743256 Z"/>
|
||||
</g>
|
||||
</svg>"""
|
||||
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────
|
||||
|
||||
def main() -> None:
|
||||
ASSETS.mkdir(parents=True, exist_ok=True)
|
||||
image = build_icon()
|
||||
save_png_and_ico(image)
|
||||
save_icns(image)
|
||||
build_tray_icons()
|
||||
print(f"Generated: {PNG_PATH}, {ICO_PATH}, {ICNS_PATH}")
|
||||
print(f"Generated tray icons in {ASSETS}")
|
||||
LANDING.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
generate_web_icons()
|
||||
generate_app_icons()
|
||||
generate_tray_icons()
|
||||
|
||||
print("\nDone! All icons generated.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in new issue
Block a user