326 lines
12 KiB
Python
326 lines
12 KiB
Python
import os
|
|
import math
|
|
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
|
|
|
def create_super_sampled_icon(size=1024, supersample=4):
|
|
ss_size = size * supersample
|
|
img = Image.new("RGBA", (ss_size, ss_size), (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
# 1. Background Rounded Squircle with rich forest-emerald gradient
|
|
# Gradient from top-left (#1F8A5B) to bottom-right (#0D452B)
|
|
radius = int(ss_size * 0.22)
|
|
|
|
# Create mask for squircle
|
|
mask = Image.new("L", (ss_size, ss_size), 0)
|
|
mask_draw = ImageDraw.Draw(mask)
|
|
margin = int(ss_size * 0.04)
|
|
mask_draw.rounded_rectangle(
|
|
[margin, margin, ss_size - margin, ss_size - margin],
|
|
radius=radius,
|
|
fill=255
|
|
)
|
|
|
|
# Base gradient image
|
|
gradient = Image.new("RGBA", (ss_size, ss_size), (0, 0, 0, 0))
|
|
c_tl = (38, 155, 102) # #269B66
|
|
c_br = (14, 66, 42) # #0E422A
|
|
|
|
for y in range(ss_size):
|
|
for x in range(0, ss_size, 4):
|
|
t = (x + y) / (ss_size * 2)
|
|
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)
|
|
# fill horizontal run
|
|
for dx in range(4):
|
|
if x + dx < ss_size:
|
|
gradient.putpixel((x + dx, y), (r, g, b, 255))
|
|
|
|
# Composite gradient with squircle mask
|
|
bg = Image.composite(gradient, Image.new("RGBA", (ss_size, ss_size), (0, 0, 0, 0)), mask)
|
|
|
|
# Inner soft glow ring
|
|
inner_glow = Image.new("RGBA", (ss_size, ss_size), (0, 0, 0, 0))
|
|
glow_draw = ImageDraw.Draw(inner_glow)
|
|
glow_draw.rounded_rectangle(
|
|
[margin + 6 * supersample, margin + 6 * supersample, ss_size - margin - 6 * supersample, ss_size - margin - 6 * supersample],
|
|
radius=radius - 6 * supersample,
|
|
outline=(255, 255, 255, 45),
|
|
width=int(3 * supersample)
|
|
)
|
|
bg = Image.alpha_composite(bg, inner_glow)
|
|
|
|
# 2. Main Speech Bubble + Sprout Motif
|
|
# Center coordinates
|
|
cx, cy = ss_size // 2, ss_size // 2
|
|
|
|
# A subtle companion speech bubble in the background (Mia AI partner)
|
|
comp_bubble = Image.new("RGBA", (ss_size, ss_size), (0, 0, 0, 0))
|
|
comp_draw = ImageDraw.Draw(comp_bubble)
|
|
|
|
comp_rect = [
|
|
int(cx + 40 * supersample),
|
|
int(cy - 280 * supersample),
|
|
int(cx + 340 * supersample),
|
|
int(cy - 20 * supersample)
|
|
]
|
|
comp_draw.rounded_rectangle(
|
|
comp_rect,
|
|
radius=int(60 * supersample),
|
|
fill=(226, 243, 232, 180) # AppColors.softGreen with opacity
|
|
)
|
|
# Companion bubble tail
|
|
comp_tail = [
|
|
(int(cx + 300 * supersample), int(cy - 40 * supersample)),
|
|
(int(cx + 360 * supersample), int(cy + 40 * supersample)),
|
|
(int(cx + 250 * supersample), int(cy - 20 * supersample)),
|
|
]
|
|
comp_draw.polygon(comp_tail, fill=(226, 243, 232, 180))
|
|
|
|
# Sound wave dots in companion bubble
|
|
dot_color = (23, 107, 70, 220)
|
|
for i in range(3):
|
|
dx_dot = int(cx + (140 + i * 55) * supersample)
|
|
dy_dot = int(cy - 150 * supersample)
|
|
r_dot = int((10 + (1 if i==1 else 0)*4) * supersample)
|
|
comp_draw.ellipse([dx_dot - r_dot, dy_dot - r_dot, dx_dot + r_dot, dy_dot + r_dot], fill=dot_color)
|
|
|
|
# Composite companion bubble
|
|
bg = Image.alpha_composite(bg, comp_bubble)
|
|
|
|
# 3. Primary Speech Bubble (Clean Crisp White with subtle drop shadow)
|
|
primary_shadow = Image.new("RGBA", (ss_size, ss_size), (0, 0, 0, 0))
|
|
sh_draw = ImageDraw.Draw(primary_shadow)
|
|
|
|
b_left = int(cx - 320 * supersample)
|
|
b_top = int(cy - 180 * supersample)
|
|
b_right = int(cx + 180 * supersample)
|
|
b_bottom = int(cy + 260 * supersample)
|
|
b_radius = int(100 * supersample)
|
|
|
|
sh_draw.rounded_rectangle(
|
|
[b_left, b_top + 16 * supersample, b_right, b_bottom + 16 * supersample],
|
|
radius=b_radius,
|
|
fill=(0, 0, 0, 70)
|
|
)
|
|
# Primary tail shadow
|
|
p_tail_sh = [
|
|
(int(b_left + 80 * supersample), int(b_bottom + 10 * supersample)),
|
|
(int(b_left - 30 * supersample), int(b_bottom + 120 * supersample)),
|
|
(int(b_left + 190 * supersample), int(b_bottom + 10 * supersample)),
|
|
]
|
|
sh_draw.polygon(p_tail_sh, fill=(0, 0, 0, 70))
|
|
primary_shadow = primary_shadow.filter(ImageFilter.GaussianBlur(radius=int(16 * supersample)))
|
|
bg = Image.alpha_composite(bg, primary_shadow)
|
|
|
|
# Draw actual Primary Bubble
|
|
primary_bubble = Image.new("RGBA", (ss_size, ss_size), (0, 0, 0, 0))
|
|
pb_draw = ImageDraw.Draw(primary_bubble)
|
|
pb_draw.rounded_rectangle(
|
|
[b_left, b_top, b_right, b_bottom],
|
|
radius=b_radius,
|
|
fill=(255, 255, 255, 255)
|
|
)
|
|
p_tail = [
|
|
(int(b_left + 80 * supersample), int(b_bottom - 10 * supersample)),
|
|
(int(b_left - 30 * supersample), int(b_bottom + 95 * supersample)),
|
|
(int(b_left + 190 * supersample), int(b_bottom - 10 * supersample)),
|
|
]
|
|
pb_draw.polygon(p_tail, fill=(255, 255, 255, 255))
|
|
bg = Image.alpha_composite(bg, primary_bubble)
|
|
|
|
# 4. Sprout & Dynamic Voice Wave Graphic inside Primary Bubble
|
|
# Center of primary bubble
|
|
pcx = (b_left + b_right) // 2
|
|
pcy = (b_top + b_bottom) // 2 - int(10 * supersample)
|
|
|
|
icon_art = Image.new("RGBA", (ss_size, ss_size), (0, 0, 0, 0))
|
|
art_draw = ImageDraw.Draw(icon_art)
|
|
|
|
# Sprout Stem & Leaves (Vibrant Emerald & Spring Green)
|
|
# Main Leaf (Right): curving upwards with life
|
|
leaf_r_points = []
|
|
steps = 40
|
|
# Center anchor of stem: (pcx - 30, pcy + 110)
|
|
stem_x, stem_y = pcx - int(20 * supersample), pcy + int(110 * supersample)
|
|
|
|
# Left leaf
|
|
left_leaf = [
|
|
(stem_x, stem_y - int(30 * supersample)),
|
|
(stem_x - int(120 * supersample), stem_y - int(60 * supersample)),
|
|
(stem_x - int(150 * supersample), stem_y - int(150 * supersample)),
|
|
(stem_x - int(60 * supersample), stem_y - int(140 * supersample)),
|
|
(stem_x, stem_y - int(70 * supersample))
|
|
]
|
|
# Draw smooth left leaf
|
|
art_draw.polygon(left_leaf, fill=(43, 174, 107, 255))
|
|
|
|
# Right bigger primary leaf
|
|
right_leaf = [
|
|
(stem_x + int(10 * supersample), stem_y - int(40 * supersample)),
|
|
(stem_x + int(70 * supersample), stem_y - int(60 * supersample)),
|
|
(stem_x + int(160 * supersample), stem_y - int(170 * supersample)),
|
|
(stem_x + int(130 * supersample), stem_y - int(220 * supersample)),
|
|
(stem_x + int(40 * supersample), stem_y - int(190 * supersample)),
|
|
(stem_x - int(10 * supersample), stem_y - int(100 * supersample))
|
|
]
|
|
art_draw.polygon(right_leaf, fill=(23, 107, 70, 255))
|
|
|
|
# Sprout Dewdrop / Energy Spark (Warm Sun Gold #F59E0B)
|
|
spark_x = stem_x + int(145 * supersample)
|
|
spark_y = stem_y - int(235 * supersample)
|
|
spark_r = int(22 * supersample)
|
|
art_draw.ellipse(
|
|
[spark_x - spark_r, spark_y - spark_r, spark_x + spark_r, spark_y + spark_r],
|
|
fill=(245, 158, 11, 255)
|
|
)
|
|
|
|
# 3 Arched Voice Waves radiating from the sprout (representing speaking & pronunciation)
|
|
wave_color = (23, 107, 70, 220)
|
|
|
|
# Wave 1 (inner)
|
|
w1_box = [
|
|
int(pcx - 180 * supersample),
|
|
int(pcy - 160 * supersample),
|
|
int(pcx - 20 * supersample),
|
|
int(pcy + 0 * supersample)
|
|
]
|
|
art_draw.arc(w1_box, start=140, end=270, fill=wave_color, width=int(14 * supersample))
|
|
|
|
# Wave 2 (middle)
|
|
w2_box = [
|
|
int(pcx - 240 * supersample),
|
|
int(pcy - 210 * supersample),
|
|
int(pcx - 10 * supersample),
|
|
int(pcy + 30 * supersample)
|
|
]
|
|
art_draw.arc(w2_box, start=145, end=265, fill=(43, 174, 107, 200), width=int(14 * supersample))
|
|
|
|
# Wave 3 (outer)
|
|
w3_box = [
|
|
int(pcx - 295 * supersample),
|
|
int(pcy - 260 * supersample),
|
|
int(pcx - 0 * supersample),
|
|
int(pcy + 60 * supersample)
|
|
]
|
|
art_draw.arc(w3_box, start=150, end=260, fill=(245, 158, 11, 230), width=int(13 * supersample))
|
|
|
|
# Composite artwork
|
|
bg = Image.alpha_composite(bg, icon_art)
|
|
|
|
# Downsample using high quality Lanczos filter for razor-sharp antialiasing
|
|
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)
|
|
|
|
# Background subtle decorative circles
|
|
dec_draw = ImageDraw.Draw(banner)
|
|
dec_draw.ellipse([800, -100, 1400, 500], fill=(226, 243, 232, 120))
|
|
dec_draw.ellipse([-100, 300, 400, 800], fill=(255, 240, 227, 120))
|
|
|
|
# Paste resized icon on the left
|
|
icon_w = 340
|
|
icon_resized = icon_img.resize((icon_w, icon_w), Image.Resampling.LANCZOS)
|
|
banner.paste(icon_resized, (100, (h - icon_w) // 2), icon_resized)
|
|
|
|
# Typography on the right
|
|
# Try finding available fonts or use default with clear layout
|
|
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
|
|
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, 30, index=0)
|
|
sub_font = ImageFont.truetype(p, 24, index=0)
|
|
badge_font = ImageFont.truetype(p, 18, 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()
|
|
badge_font = ImageFont.load_default()
|
|
|
|
tx = 490
|
|
ty = 135
|
|
|
|
# Tag / Pill: "AI 伴学 · 零基础开口"
|
|
pill_w = 210
|
|
pill_h = 36
|
|
draw.rounded_rectangle([tx, ty, tx + pill_w, ty + pill_h], radius=18, fill=(226, 243, 232, 255))
|
|
draw.text((tx + 18, ty + 7), "🌱 AI 伴学 · 轻松开口", fill=(23, 107, 70, 255), font=badge_font)
|
|
|
|
# Main Brand Name
|
|
draw.text((tx, ty + 50), "芽说英语", fill=(25, 33, 27, 255), font=title_font)
|
|
|
|
# English Name
|
|
draw.text((tx + 270, ty + 72), "SpeakSprout", fill=(23, 107, 70, 255), font=en_font)
|
|
|
|
# Slogan / Value Proposition
|
|
draw.text((tx, ty + 145), "每一次开口,都是成长的萌芽", fill=(100, 114, 104, 255), font=sub_font)
|
|
draw.text((tx, ty + 190), "• 真实场景 1v1 AI 语伴 Mia", fill=(25, 33, 27, 230), font=sub_font)
|
|
draw.text((tx, ty + 235), "• 听 / 说 / 读 / 写 四维科学进阶", fill=(25, 33, 27, 230), font=sub_font)
|
|
draw.text((tx, ty + 280), "• 本地离线高精语音识别 · 极速跟读", fill=(25, 33, 27, 230), font=sub_font)
|
|
|
|
return banner
|
|
|
|
if __name__ == "__main__":
|
|
out_dir = "assets/branding"
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
|
|
print("🎨 正在生成超采样高清 Logo (1024x1024)...")
|
|
icon1024 = create_super_sampled_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 和图标生成完成!")
|