diff --git a/public/favicon.ico b/public/favicon.ico index 57614f9..82a000e 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/src/generate-logo.py b/src/generate-logo.py new file mode 100644 index 0000000..1dc1920 --- /dev/null +++ b/src/generate-logo.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +""" +Logo & Favicon Generator +Extracted from CSS color scheme: + Background: #161b22 (dark navy) + Border: #30363d (dark grey) + Primary text: #e6edf3 (near-white) + Secondary: #c9d1d9 (light grey) + Muted: #8b949e (medium grey) + Accent: #2f81f7 (vivid blue) + +Outputs: + logo.png — 600 × 180 px (wide wordmark) + logo@2x.png — 1200 × 360 px (retina) + favicon.ico — 16×16, 32×32, 48×48 multi-size ICO + favicon-32.png + favicon-16.png +""" + +from PIL import Image, ImageDraw, ImageFont +import math +import os +import struct +import io + +# ── Palette ──────────────────────────────────────────────────────────────────── +BG = (22, 27, 34, 255) # #161b22 +BORDER = (48, 54, 61, 255) # #30363d +TEXT_PRI = (230, 237, 243, 255) # #e6edf3 +TEXT_SEC = (201, 209, 217, 255) # #c9d1d9 +TEXT_MUTE = (139, 148, 158, 255) # #8b949e +ACCENT = (47, 129, 247, 255) # #2f81f7 +ACCENT_DIM= (47, 129, 247, 36) # translucent blue fill + +# ── Helpers ──────────────────────────────────────────────────────────────────── + +def lerp_color(a, b, t): + return tuple(int(a[i] + (b[i] - a[i]) * t) for i in range(4)) + + +def draw_rounded_rect(draw, xy, radius, fill=None, outline=None, width=1): + x0, y0, x1, y1 = xy + draw.rounded_rectangle([x0, y0, x1, y1], radius=radius, fill=fill, + outline=outline, width=width) + + +def add_glow(img, center, radius, color, intensity=0.55): + """Soft radial glow overlay using additive blending.""" + glow = Image.new("RGBA", img.size, (0, 0, 0, 0)) + draw = ImageDraw.Draw(glow) + cx, cy = center + steps = 18 + for i in range(steps, 0, -1): + t = i / steps + r = int(radius * t) + alpha = int(intensity * (1 - t) ** 0.6 * 255) + c = (*color[:3], alpha) + draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill=c) + return Image.alpha_composite(img, glow) + + +def make_font(size): + """Try to load a system sans-serif; fall back to PIL default.""" + candidates = [ + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", + "/usr/share/fonts/truetype/freefont/FreeSansBold.ttf", + "/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf", + "/usr/share/fonts/truetype/ubuntu/Ubuntu-B.ttf", + "/System/Library/Fonts/Helvetica.ttc", + "C:/Windows/Fonts/arialbd.ttf", + "C:/Windows/Fonts/calibrib.ttf", + ] + for path in candidates: + if os.path.exists(path): + try: + return ImageFont.truetype(path, size) + except OSError: + pass + + # Ask fontconfig for whatever sans-serif is available + try: + import subprocess + result = subprocess.run( + ["fc-match", "--format=%{file}", "sans-serif:bold"], + capture_output=True, text=True + ) + path = result.stdout.strip() + if path: + return ImageFont.truetype(path, size) + except Exception: + pass + return ImageFont.load_default(size) + + +def make_font_regular(size): + candidates = [ + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", + "/usr/share/fonts/truetype/freefont/FreeSans.ttf", + "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf", + "/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf", + "/System/Library/Fonts/Helvetica.ttc", + "C:/Windows/Fonts/arial.ttf", + "C:/Windows/Fonts/calibri.ttf", + ] + for path in candidates: + if os.path.exists(path): + try: + return ImageFont.truetype(path, size) + except OSError: + pass + # Ask fontconfig for whatever sans-serif is available + try: + import subprocess + result = subprocess.run( + ["fc-match", "--format=%{file}", "sans-serif"], + capture_output=True, text=True + ) + path = result.stdout.strip() + if path: + return ImageFont.truetype(path, size) + except Exception: + pass + return ImageFont.load_default(size) + +# ── Icon mark (the "gem" shape) ──────────────────────────────────────────────── + +def draw_icon_mark(draw, img, cx, cy, size): + """ + Draw a stylised hexagonal mark: + - dark fill with blue border + - inner accent diamond + - subtle glow + """ + s = size + # Flat-top hexagon vertices + hex_pts = [ + (cx + s * math.cos(math.radians(a)), + cy + s * math.sin(math.radians(a))) + for a in range(0, 360, 60) + ] + + # Fill with subtle gradient-like layering + for layer in range(int(s), 0, -1): + t = layer / s + col = lerp_color( + (35, 43, 54, 255), + (22, 27, 34, 255), + t + ) + pts = [ + (cx + layer * math.cos(math.radians(a)), + cy + layer * math.sin(math.radians(a))) + for a in range(0, 360, 60) + ] + draw.polygon(pts, fill=col) + + # Border + draw.polygon(hex_pts, outline=ACCENT[:3] + (200,), width=max(2, size // 18)) + + # Inner diamond + d = s * 0.42 + diamond = [ + (cx, cy - d), + (cx + d, cy), + (cx, cy + d), + (cx - d, cy), + ] + draw.polygon(diamond, fill=ACCENT[:3] + (230,)) + + # Tiny highlight dot + h = s * 0.12 + draw.ellipse([cx - h, cy - h - d * 0.28, + cx + h, cy + h - d * 0.28], + fill=(255, 255, 255, 180)) + + return img + +# ── Logo ─────────────────────────────────────────────────────────────────────── + +def build_logo(width=600, height=180, scale=1): + W, H = width * scale, height * scale + img = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + + pad = int(16 * scale) + radius = int(20 * scale) + + # Background card + draw_rounded_rect(draw, + [pad, pad, W - pad, H - pad], + radius=radius, + fill=BG, + outline=BORDER[:3], + width=max(1, scale)) + + icon_cx = int(H * 0.5) + icon_cy = int(H * 0.5) + icon_r = int(H * 0.28) + + # Glow clipped to card bounds + card = img.crop([pad, pad, W - pad, H - pad]) + glow_cx = icon_cx - pad + glow_cy = icon_cy - pad + card = add_glow(card, (glow_cx, glow_cy), int(icon_r * 1.6), ACCENT, 0.28) + img.paste(card, (pad, pad)) + + draw = ImageDraw.Draw(img) + + # Icon mark + draw_icon_mark(draw, img, icon_cx, icon_cy, icon_r) + draw = ImageDraw.Draw(img) # refresh after composite + + # Text area + text_x = icon_cx + icon_r + int(22 * scale) + brand_font = make_font(int(38 * scale)) + sub_font = make_font_regular(int(16 * scale)) + + brand_text = "YourBrand" + sub_text = "Your tagline here" + + # Brand name + draw.text((text_x, int(H * 0.22)), brand_text, + font=brand_font, fill=TEXT_PRI) + + # Accent underline + bbox = draw.textbbox((text_x, int(H * 0.22)), brand_text, font=brand_font) + uw = bbox[2] - bbox[0] + uy = bbox[3] + int(4 * scale) + + draw.rectangle([text_x, uy, text_x + uw, uy + max(2, int(3 * scale))], + fill=ACCENT[:3]) + + # Subtitle + draw.text((text_x, int(H * 0.63)), sub_text, + font=sub_font, fill=TEXT_MUTE) + + # Thin right-side accent bar + bar_x = W - pad - max(3, int(4 * scale)) + draw.rectangle([bar_x, pad + radius, + bar_x + max(3, int(4 * scale)), H - pad - radius], + fill=ACCENT[:3] + (120,)) + + return img + +# ── Favicon mark ─────────────────────────────────────────────────────────────── + +def build_favicon_image(size): + img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + + pad = max(1, size // 24) + radius = max(2, size // 8) + + # Background + draw_rounded_rect(draw, + [pad, pad, size - pad, size - pad], + radius=radius, + fill=BG, + outline=BORDER[:3], + width=max(1, size // 24)) + + cx, cy = size // 2, size // 2 + icon_r = int(size * 0.30) + + card = img.crop([pad, pad, size - pad, size - pad]) + card = add_glow(card, (cx - pad, cy - pad), int(icon_r * 1.6), ACCENT, 0.28) + img.paste(card, (pad, pad)) + + draw = ImageDraw.Draw(img) + draw_icon_mark(draw, img, cx, cy, icon_r) + + return img + +# ── ICO writer ───────────────────────────────────────────────────────────────── + +def save_ico(images_dict, path): + """ + images_dict: {size: PIL.Image, ...} e.g. {16: img16, 32: img32, 48: img48} + """ + sizes = sorted(images_dict.keys()) + entries = [] + png_data_list = [] + + for s in sizes: + img = images_dict[s].convert("RGBA") + buf = io.BytesIO() + img.save(buf, format="PNG") + data = buf.getvalue() + png_data_list.append(data) + entries.append((s, len(data))) + + # ICO header: RESERVED(2) TYPE(2) COUNT(2) + header = struct.pack("