feat: 完善芽说英语品牌Logo与图标配置,完成跨平台云端同步服务开发与自动化部署
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
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 和图标生成完成!")
|
||||
Reference in New Issue
Block a user