import os import math from PIL import Image, ImageDraw, ImageFont, ImageFilter def cubic_bezier(p0, p1, p2, p3, steps=60): points = [] for i in range(steps + 1): t = i / float(steps) x = (1-t)**3 * p0[0] + 3*(1-t)**2 * t * p1[0] + 3*(1-t) * t**2 * p2[0] + t**3 * p3[0] y = (1-t)**3 * p0[1] + 3*(1-t)**2 * t * p1[1] + 3*(1-t) * t**2 * p2[1] + t**3 * p3[1] points.append((x, y)) return points def create_sprout_icon(size=1024, supersample=4, transparent_bg=False): ss = size * supersample img = Image.new("RGBA", (ss, ss), (0, 0, 0, 0)) radius = int(ss * 0.225) margin = int(ss * 0.045) mask = Image.new("L", (ss, ss), 0) mask_draw = ImageDraw.Draw(mask) mask_draw.rounded_rectangle( [margin, margin, ss - margin, ss - margin], radius=radius, fill=255 ) gradient = Image.new("RGBA", (ss, ss), (0, 0, 0, 0)) # Colors: top-left fresh emerald #198754, bottom-right deep forest #0D4E30 c_tl = (28, 148, 92) c_br = (12, 68, 42) for y in range(ss): for x in range(0, ss, 4): t = (x * 0.7 + y * 1.0) / (ss * 1.7) t = max(0.0, min(1.0, t)) r = int(c_tl[0] * (1 - t) + c_br[0] * t) g = int(c_tl[1] * (1 - t) + c_br[1] * t) b = int(c_tl[2] * (1 - t) + c_br[2] * t) for dx in range(4): if x + dx < ss: gradient.putpixel((x + dx, y), (r, g, b, 255)) bg = Image.composite(gradient, Image.new("RGBA", (ss, ss), (0, 0, 0, 0)), mask) # Ambient subtle highlight on top edge highlight = Image.new("RGBA", (ss, ss), (0, 0, 0, 0)) hl_draw = ImageDraw.Draw(highlight) hl_draw.rounded_rectangle( [margin + 4*supersample, margin + 4*supersample, ss - margin - 4*supersample, ss - margin - 4*supersample], radius=radius - 4*supersample, outline=(255, 255, 255, 35), width=int(2.5 * supersample) ) bg = Image.alpha_composite(bg, highlight) cx, cy = ss // 2, ss // 2 # 2. Main Center Hero Badge: Rounded Speech Bubble with clean geometry bw = int(580 * supersample) bh = int(480 * supersample) bx = cx - bw // 2 by = cy - bh // 2 - int(25 * supersample) br = int(120 * supersample) # Bubble Drop Shadow bubble_shadow = Image.new("RGBA", (ss, ss), (0, 0, 0, 0)) bsh_draw = ImageDraw.Draw(bubble_shadow) bsh_draw.rounded_rectangle( [bx, by + int(18 * supersample), bx + bw, by + bh + int(18 * supersample)], radius=br, fill=(0, 0, 0, 75) ) tail_pts_sh = [ (bx + int(90 * supersample), by + bh + int(10 * supersample)), (bx + int(30 * supersample), by + bh + int(105 * supersample)), (bx + int(190 * supersample), by + bh + int(10 * supersample)), ] bsh_draw.polygon(tail_pts_sh, fill=(0, 0, 0, 75)) bubble_shadow = bubble_shadow.filter(ImageFilter.GaussianBlur(radius=int(18 * supersample))) bg = Image.alpha_composite(bg, bubble_shadow) # Bubble Body (Pure White with subtle warm sheen) bubble = Image.new("RGBA", (ss, ss), (0, 0, 0, 0)) b_draw = ImageDraw.Draw(bubble) b_draw.rounded_rectangle( [bx, by, bx + bw, by + bh], radius=br, fill=(255, 255, 255, 255) ) tail_pts = [ (bx + int(90 * supersample), by + bh - int(8 * supersample)), (bx + int(30 * supersample), by + bh + int(85 * supersample)), (bx + int(190 * supersample), by + bh - int(8 * supersample)), ] b_draw.polygon(tail_pts, fill=(255, 255, 255, 255)) bg = Image.alpha_composite(bg, bubble) # 3. Inside the Bubble: Sprout + Smile Soundwave Iconography icx = bx + bw // 2 icy = by + bh // 2 - int(10 * supersample) sprout_layer = Image.new("RGBA", (ss, ss), (0, 0, 0, 0)) sp_draw = ImageDraw.Draw(sprout_layer) # 3.1 Right Leaf (Primary, lush emerald) p0 = (icx - int(10 * supersample), icy + int(35 * supersample)) p1 = (icx + int(70 * supersample), icy + int(15 * supersample)) p2 = (icx + int(165 * supersample), icy - int(50 * supersample)) p3 = (icx + int(135 * supersample), icy - int(130 * supersample)) # Tip p4 = (icx + int(60 * supersample), icy - int(115 * supersample)) p5 = (icx + int(0 * supersample), icy - int(45 * supersample)) p6 = p0 curve_right_top = cubic_bezier(p0, p1, p2, p3, steps=40) curve_right_bot = cubic_bezier(p3, p4, p5, p6, steps=40) right_leaf_pts = curve_right_top + curve_right_bot sp_draw.polygon(right_leaf_pts, fill=(23, 107, 70, 255)) # #176B46 # 3.2 Left Leaf (Secondary, fresh bright mint green) lp0 = (icx - int(18 * supersample), icy + int(50 * supersample)) lp1 = (icx - int(65 * supersample), icy + int(25 * supersample)) lp2 = (icx - int(135 * supersample), icy - int(15 * supersample)) lp3 = (icx - int(120 * supersample), icy - int(80 * supersample)) # Tip lp4 = (icx - int(60 * supersample), icy - int(65 * supersample)) lp5 = (icx - int(15 * supersample), icy - int(10 * supersample)) lp6 = lp0 curve_left_top = cubic_bezier(lp0, lp1, lp2, lp3, steps=40) curve_left_bot = cubic_bezier(lp3, lp4, lp5, lp6, steps=40) left_leaf_pts = curve_left_top + curve_left_bot sp_draw.polygon(left_leaf_pts, fill=(52, 185, 118, 255)) # Bright fresh green # 3.3 Sprout Sun Dot / Golden Energy Droplet (Golden Sun #F59E0B) dot_x = icx + int(148 * supersample) dot_y = icy - int(142 * supersample) dot_r = int(22 * supersample) sp_draw.ellipse( [dot_x - dot_r, dot_y - dot_r, dot_x + dot_r, dot_y + dot_r], fill=(245, 158, 11, 255) ) # 3.4 Smile / Vocal Flow Arc smile_y = icy + int(135 * supersample) smile_w = int(140 * supersample) smile_pts = cubic_bezier( (icx - smile_w, smile_y - int(15 * supersample)), (icx - smile_w // 2, smile_y + int(30 * supersample)), (icx + smile_w // 2, smile_y + int(30 * supersample)), (icx + smile_w, smile_y - int(15 * supersample)), steps=50 ) for i in range(len(smile_pts) - 1): sp_draw.line([smile_pts[i], smile_pts[i+1]], fill=(23, 107, 70, 230), width=int(14 * supersample)) s_cap_r = int(7 * supersample) sp_draw.ellipse([smile_pts[0][0]-s_cap_r, smile_pts[0][1]-s_cap_r, smile_pts[0][0]+s_cap_r, smile_pts[0][1]+s_cap_r], fill=(23, 107, 70, 230)) sp_draw.ellipse([smile_pts[-1][0]-s_cap_r, smile_pts[-1][1]-s_cap_r, smile_pts[-1][0]+s_cap_r, smile_pts[-1][0]+s_cap_r], fill=(23, 107, 70, 230)) # 3.5 Concentric Dynamic Sound Waves w_cx = icx - int(55 * supersample) w_cy = icy - int(55 * supersample) wave_r1 = int(115 * supersample) sp_draw.arc( [w_cx - wave_r1, w_cy - wave_r1, w_cx + wave_r1, w_cy + wave_r1], start=155, end=245, fill=(52, 185, 118, 240), width=int(12 * supersample) ) wave_r2 = int(165 * supersample) sp_draw.arc( [w_cx - wave_r2, w_cy - wave_r2, w_cx + wave_r2, w_cy + wave_r2], start=160, end=240, fill=(245, 158, 11, 230), width=int(12 * supersample) ) bg = Image.alpha_composite(bg, sprout_layer) # Downsample final_icon = bg.resize((size, size), Image.Resampling.LANCZOS) return final_icon def create_brand_banner(icon_img): w, h = 1200, 600 banner = Image.new("RGBA", (w, h), (245, 247, 243, 255)) # AppColors.paper #F5F7F3 draw = ImageDraw.Draw(banner) # Decorative background shapes draw.ellipse([760, -120, 1360, 480], fill=(226, 243, 232, 140)) draw.ellipse([-80, 320, 420, 820], fill=(255, 240, 227, 130)) # Left Icon icon_w = 340 icon_resized = icon_img.resize((icon_w, icon_w), Image.Resampling.LANCZOS) banner.paste(icon_resized, (90, (h - icon_w) // 2), icon_resized) font_path_candidates = [ "/System/Library/Fonts/PingFang.ttc", "/System/Library/Fonts/Hiragino Sans GB.ttc", "/System/Library/Fonts/Supplemental/Arial Bold.ttf", "/System/Library/Fonts/Supplemental/Arial.ttf" ] title_font = None en_font = None sub_font = None desc_font = None badge_font = None for p in font_path_candidates: if os.path.exists(p): try: title_font = ImageFont.truetype(p, 58, index=0) en_font = ImageFont.truetype(p, 32, index=0) sub_font = ImageFont.truetype(p, 23, index=0) desc_font = ImageFont.truetype(p, 21, index=0) badge_font = ImageFont.truetype(p, 17, index=0) break except Exception: continue if not title_font: title_font = ImageFont.load_default() en_font = ImageFont.load_default() sub_font = ImageFont.load_default() desc_font = ImageFont.load_default() badge_font = ImageFont.load_default() tx = 475 ty = 105 # Badge Pill pill_w = 240 pill_h = 36 draw.rounded_rectangle([tx, ty, tx + pill_w, ty + pill_h], radius=18, fill=(226, 243, 232, 255)) # Small green dot in pill draw.ellipse([tx + 14, ty + 12, tx + 26, ty + 24], fill=(23, 107, 70, 255)) draw.text((tx + 34, ty + 7), "AI 口语私教 · 零基础开口", fill=(23, 107, 70, 255), font=badge_font) # Main Brand Name draw.text((tx, ty + 52), "芽说英语", fill=(25, 33, 27, 255), font=title_font) draw.text((tx + 270, ty + 72), "SpeakSprout", fill=(23, 107, 70, 255), font=en_font) # Slogan draw.text((tx, ty + 145), "“ 每一次开口,都是成长的萌芽 ”", fill=(100, 114, 104, 255), font=sub_font) # Clean bullet features (using custom drawn colored dots instead of emoji glyphs) features = [ ("1v1 智能语伴 Mia · 沉浸式情境真实对话", (23, 107, 70, 255)), ("听 · 说 · 读 · 写 四维微习惯进阶体系", (52, 185, 118, 255)), ("离线高精度语音识别 · 毫秒级跟读评分", (245, 158, 11, 255)), ("间隔艾宾浩斯复习 · 真正打破哑巴英语", (23, 107, 70, 255)) ] for i, (feat, dot_c) in enumerate(features): fy = ty + 200 + i * 44 # Draw small clean bullet dot draw.ellipse([tx, fy + 7, tx + 10, fy + 17], fill=dot_c) draw.text((tx + 22, fy), feat, fill=(35, 45, 38, 245), font=desc_font) return banner if __name__ == "__main__": out_dir = "assets/branding" os.makedirs(out_dir, exist_ok=True) print("🎨 正在生成全新高清 Logo v2 (1024x1024)...") icon1024 = create_sprout_icon(size=1024, supersample=4) master_path = os.path.join(out_dir, "logo_master_1024.png") icon1024.save(master_path, "PNG") print(f"✅ 保存主图标: {master_path}") # 512x512 icon512 = icon1024.resize((512, 512), Image.Resampling.LANCZOS) p512 = os.path.join(out_dir, "logo_512.png") icon512.save(p512, "PNG") # Brand Banner print("🖼️ 正在生成品牌展示图 (1200x600)...") banner = create_brand_banner(icon1024) banner_path = os.path.join(out_dir, "brand_banner.png") banner.save(banner_path, "PNG") print(f"✅ 保存品牌展示图: {banner_path}") # Android launcher icons mipmaps = { "mipmap-mdpi": 48, "mipmap-hdpi": 72, "mipmap-xhdpi": 96, "mipmap-xxhdpi": 144, "mipmap-xxxhdpi": 192, } print("📱 正在更新 Android App 各分辨率图标...") for folder, sz in mipmaps.items(): dir_path = os.path.join("android/app/src/main/res", folder) os.makedirs(dir_path, exist_ok=True) out_f = os.path.join(dir_path, "ic_launcher.png") resized = icon1024.resize((sz, sz), Image.Resampling.LANCZOS) resized.save(out_f, "PNG") print(f" -> {out_f} ({sz}x{sz})") print("🎉 全部 Logo 和图标生成完成!")