Files
English/kouyu_english/generate_logo_v2.py
T

309 lines
12 KiB
Python

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 和图标生成完成!")