feat: redesign miniapp tabbar icons

This commit is contained in:
2026-09-10 22:00:17 +08:00
parent d6f5f25c35
commit 4dd93db137
44 changed files with 174 additions and 307 deletions
+98 -220
View File
@@ -1,236 +1,114 @@
#!/usr/bin/env python3
"""
丹阳公益婚恋平台 - 底部导航栏图标生成脚本
圆润线性风格,两种状态(未选中/选中),三种尺寸
"""
"""生成善缘丹阳小程序底部导航 SVG 与透明 PNG。"""
from pathlib import Path
import math
from PIL import Image, ImageDraw
import math
import os
# 主题色配置
COLOR_UNSELECTED = (153, 153, 153) # #999999
COLOR_SELECTED = (233, 107, 107) # #E96B6B
COLOR_BG = (255, 255, 255) # 白色背景
ROOT = Path(__file__).resolve().parents[1] / "miniapp" / "assets" / "tabbar"
COLORS = {"normal": "#999999", "active": "#E96B6B"}
TITLES = {
"home": "爱心小屋",
"discover": "双人缘分",
"message": "爱心对话",
"activity": "公益日历",
"mine": "个人守护",
}
def sin_deg(x):
return math.sin(math.radians(x))
def cos_deg(x):
return math.cos(math.radians(x))
def draw_heart_points(cx, cy, scale):
"""计算心形路径点"""
pts = []
for t in range(0, 360, 3):
rad = t * 3.14159 / 180
x = scale * 16 * (sin_deg(rad) ** 3)
y = -scale * (13 * cos_deg(rad) - 5 * cos_deg(2*rad) - 2 * cos_deg(3*rad) - cos_deg(4*rad))
pts.append((cx + x, cy + y))
return pts
def draw_rounded_rect(draw, x, y, w, h, r, color, width=2):
"""绘制圆角矩形(无填充,只描边)"""
r = min(r, w // 2, h // 2)
draw.rounded_rectangle([x, y, x + w, y + h], radius=r, outline=color, width=width)
def draw_house(draw, cx, cy, size, color, width=2):
"""绘制房子图标 - 首页"""
s = size * 0.35
# 房子主体
body_x, body_y = cx - s * 0.7, cy - s * 0.1
body_w, body_h = s * 1.4, s * 0.7
draw_rounded_rect(draw, body_x, body_y, body_w, body_h, 4, color, width)
# 屋顶(三角形)
roof_points = [
(cx, body_y - s * 0.5),
(body_x - s * 0.15, body_y),
(body_x + body_w + s * 0.15, body_y)
]
draw.polygon(roof_points, outline=color, width=width)
# 门
door_w, door_h = s * 0.28, s * 0.4
door_x = cx - door_w / 2
door_y = body_y + body_h - door_h
draw_rounded_rect(draw, door_x, door_y, door_w, door_h, 2, color, width)
# 烟囱
chimney_w, chimney_h = s * 0.12, s * 0.22
chimney_x = cx + s * 0.35
chimney_y = body_y - s * 0.35
draw_rounded_rect(draw, chimney_x, chimney_y, chimney_w, chimney_h, 2, color, width)
def draw_hearts(draw, cx, cy, size, color, width=2):
"""绘制双心图标 - 找对象"""
s = size * 0.016
offset = s * 4
# 左心
left_pts = draw_heart_points(cx - offset, cy - offset * 0.3, s)
right_pts = draw_heart_points(cx + offset, cy - offset * 0.3, s)
for pts in [left_pts, right_pts]:
for i in range(len(pts)):
p1 = pts[i]
p2 = pts[(i + 1) % len(pts)]
draw.line([p1, p2], fill=color, width=width)
def draw_chat_bubble(draw, cx, cy, size, color, width=2):
"""绘制对话气泡图标 - 消息"""
s = size * 0.28
# 气泡主体
bubble_x, bubble_y = cx - s, cy - s * 0.6
bubble_w, bubble_h = s * 2, s * 1.2
draw_rounded_rect(draw, bubble_x, bubble_y, bubble_w, bubble_h, 8, color, width)
# 小三角
tail_points = [
(bubble_x + s * 0.1, bubble_y + bubble_h),
(bubble_x + s * 0.3, bubble_y + bubble_h + s * 0.25),
(bubble_x + s * 0.5, bubble_y + bubble_h)
]
draw.polygon(tail_points, outline=color, width=width)
# 内部文字线条
line_y_start = bubble_y + s * 0.2
for i in range(3):
line_y = line_y_start + i * s * 0.22
draw.line([(bubble_x + s * 0.15, line_y), (bubble_x + s * 1.5, line_y)], fill=color, width=width)
def draw_calendar(draw, cx, cy, size, color, width=2):
"""绘制日历图标 - 活动"""
s = size * 0.26
# 日历主体
cal_x, cal_y = cx - s, cy - s * 0.55
cal_w, cal_h = s * 2, s * 1.3
draw_rounded_rect(draw, cal_x, cal_y, cal_w, cal_h, 6, color, width)
# 顶部横线(分隔年月和日期)
draw.line([(cal_x + s * 0.05, cal_y + s * 0.25), (cal_x + cal_w - s * 0.05, cal_y + s * 0.25)], fill=color, width=width)
# 日期点
dot_r = 2 if size >= 162 else 1
positions = [
(cal_x + s * 0.3, cal_y + s * 0.55),
(cal_x + s * 0.7, cal_y + s * 0.55),
(cal_x + s * 1.1, cal_y + s * 0.55),
(cal_x + s * 0.5, cal_y + s * 0.9),
(cal_x + s * 0.9, cal_y + s * 0.9),
]
for dx, dy in positions:
draw.ellipse([dx - dot_r, dy - dot_r, dx + dot_r, dy + dot_r], fill=color)
# 左上角挂环
ring_r = 4 if size >= 162 else 2
draw.ellipse([cal_x + s * 0.25 - ring_r, cal_y - ring_r, cal_x + s * 0.25 + ring_r, cal_y + ring_r], outline=color, width=width)
draw.ellipse([cal_x + s * 1.75 - ring_r, cal_y - ring_r, cal_x + s * 1.75 + ring_r, cal_y + ring_r], outline=color, width=width)
def draw_user(draw, cx, cy, size, color, width=2):
"""绘制用户图标 - 我的"""
s = size * 0.3
# 头部(圆形)
head_r = s * 0.38
head_x, head_y = cx, cy - s * 0.25
draw.ellipse([head_x - head_r, head_y - head_r, head_x + head_r, head_y + head_r], outline=color, width=width)
# 身体(半椭圆)
body_top = cy + s * 0.15
body_bottom = cy + s * 0.75
body_left = cx - s * 0.45
body_right = cx + s * 0.45
draw.arc([body_left, body_top, body_right, body_bottom], start=180, end=360, fill=color, width=width)
# 身体两侧竖线
side_offset = s * 0.4
draw.line([(cx - side_offset, body_top), (cx - side_offset, body_bottom)], fill=color, width=width)
draw.line([(cx + side_offset, body_top), (cx + side_offset, body_bottom)], fill=color, width=width)
ICON_FUNCTIONS = {
"home": draw_house,
"discover": draw_hearts,
"message": draw_chat_bubble,
"activity": draw_calendar,
"mine": draw_user,
SVG_BODY = {
"home": """<path d="M18 38 40.5 19 63 38"/><rect x="22" y="35" width="37" height="28" rx="7"/><path d="M31 63V49h19v14"/><path class="heart" d="M40.5 54c-5-3.4-7-5.4-7-8.2 0-2.3 1.8-4.1 4.1-4.1 1.3 0 2.4.6 2.9 1.6.6-1 1.7-1.6 3-1.6 2.3 0 4.1 1.8 4.1 4.1 0 2.8-2.1 4.8-7.1 8.2Z"/>""",
"discover": """<circle cx="26" cy="29" r="7"/><circle cx="55" cy="29" r="7"/><path d="M14 55c1-10 6-15 15-15 4 0 7 1 9 3M67 55c-1-10-6-15-15-15-4 0-7 1-9 3"/><path class="heart" d="M40.5 58c-6.8-4.5-10-7.6-10-11.2 0-3 2.3-5.3 5.3-5.3 2 0 3.7 1 4.7 2.5 1-1.5 2.7-2.5 4.7-2.5 3 0 5.3 2.3 5.3 5.3 0 3.6-3.2 6.7-10 11.2Z"/>""",
"message": """<path d="M23 60 15 66l2.4-11.2A12 12 0 0 1 14 46V29a12 12 0 0 1 12-12h29a12 12 0 0 1 12 12v17a12 12 0 0 1-12 12H27c-1.4 0-2.7.7-4 2Z"/><path class="heart" d="M40.5 48c-7.5-5-11-8.4-11-12.4 0-3.3 2.6-5.9 5.9-5.9 2.2 0 4.1 1.1 5.1 2.8 1.1-1.7 3-2.8 5.2-2.8 3.3 0 5.9 2.6 5.9 5.9 0 4-3.6 7.4-11.1 12.4Z"/>""",
"activity": """<rect x="17" y="20" width="47" height="44" rx="8"/><path d="M17 33h47M28 15v10M53 15v10"/><path class="heart" d="M40.5 57c-6.5-4.3-9.5-7.2-9.5-10.7 0-2.9 2.3-5.1 5.1-5.1 1.9 0 3.5.9 4.4 2.4.9-1.5 2.5-2.4 4.5-2.4 2.8 0 5.1 2.2 5.1 5.1 0 3.5-3.1 6.4-9.6 10.7Z"/>""",
"mine": """<circle cx="40.5" cy="27" r="8"/><path d="M20 64c.7-14 8-21 20.5-21S60.3 50 61 64"/><path class="heart" d="M40.5 59c-5.2-3.5-7.7-5.8-7.7-8.6 0-2.3 1.9-4.2 4.2-4.2 1.6 0 2.8.8 3.5 2 .8-1.2 2-2 3.6-2 2.3 0 4.2 1.9 4.2 4.2 0 2.8-2.5 5.1-7.8 8.6Z"/>""",
}
def create_icon(icon_name, size, color):
"""创建单个图标"""
img = Image.new('RGBA', (size, size), color=(255, 255, 255, 0)) # 透明背景
draw = ImageDraw.Draw(img)
def svg(name: str, active: bool) -> str:
color = COLORS["active" if active else "normal"]
fill = color if active else "none"
return f"""<svg width="81" height="81" viewBox="0 0 81 81" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>{TITLES[name]}</title>
<g stroke="{color}" stroke-width="4" stroke-linecap="round" stroke-linejoin="round">{SVG_BODY[name]}</g>
<style>.heart{{fill:{fill};stroke:{color}}}.badge{{fill:white}}</style>
</svg>
"""
func = ICON_FUNCTIONS.get(icon_name)
if func:
func(draw, size // 2, size // 2, size, color)
return img
def heart_points(cx: float, cy: float, scale: float):
points = []
for degree in range(0, 361, 3):
angle = math.radians(degree)
x = scale * 16 * math.sin(angle) ** 3
y = -scale * (13 * math.cos(angle) - 5 * math.cos(2 * angle) - 2 * math.cos(3 * angle) - math.cos(4 * angle))
points.append((cx + x, cy + y))
return points
def render(name: str, size: int, active: bool) -> Image.Image:
aa = 4
factor = size / 81 * aa
image = Image.new("RGBA", (size * aa, size * aa), (255, 255, 255, 0))
draw = ImageDraw.Draw(image)
color = COLORS["active" if active else "normal"]
width = max(1, round(4 * factor))
p = lambda value: round(value * factor)
def line(points):
draw.line([(p(x), p(y)) for x, y in points], fill=color, width=width, joint="curve")
def ellipse(box, fill=None):
draw.ellipse(tuple(p(v) for v in box), outline=color, fill=fill, width=width)
def rounded(box, radius):
draw.rounded_rectangle(tuple(p(v) for v in box), radius=p(radius), outline=color, width=width)
def heart(cx, cy, scale):
points = [(p(x), p(y)) for x, y in heart_points(cx, cy, scale)]
if active:
draw.polygon(points, fill=color)
else:
draw.line(points, fill=color, width=width, joint="curve")
if name == "home":
line([(18, 38), (40.5, 19), (63, 38)])
rounded((22, 35, 59, 63), 7)
line([(31, 63), (31, 49), (50, 49), (50, 63)])
heart(40.5, 47.5, .32)
elif name == "discover":
ellipse((19, 22, 33, 36))
ellipse((48, 22, 62, 36))
draw.arc((p(14), p(39), p(43), p(67)), 190, 315, fill=color, width=width)
draw.arc((p(38), p(39), p(67), p(67)), 225, 350, fill=color, width=width)
heart(40.5, 48, .48)
elif name == "message":
rounded((14, 17, 67, 58), 12)
line([(27, 58), (15, 66), (18, 53)])
heart(40.5, 37, .52)
elif name == "activity":
rounded((17, 20, 64, 64), 8)
line([(17, 33), (64, 33)])
line([(28, 15), (28, 25)])
line([(53, 15), (53, 25)])
heart(40.5, 48, .45)
else:
ellipse((32.5, 19, 48.5, 35))
draw.arc((p(20), p(39), p(61), p(78)), 185, 355, fill=color, width=width)
heart(40.5, 52, .36)
return image.resize((size, size), Image.Resampling.LANCZOS)
def main():
output_dir = r"E:\develop\公益婚恋服务平台\miniapp\assets\tabbar"
os.makedirs(output_dir, exist_ok=True)
icons = [
("home", "首页"),
("discover", "找对象"),
("message", "消息"),
("activity", "活动"),
("mine", "我的"),
]
sizes_map = [
(81, ""), # 1x
(162, "_2x"), # 2x
(243, "_3x"), # 3x
]
print("=" * 50)
print("丹阳公益婚恋平台 - 底部导航栏图标生成")
print("=" * 50)
print()
total = 0
for icon_name, label in icons:
print(f"🎨 {label} ({icon_name}):")
for size, suffix in sizes_map:
# 未选中状态
img_u = create_icon(icon_name, size, color=COLOR_UNSELECTED)
path_u = os.path.join(output_dir, f"{icon_name}{suffix}.png")
img_u.save(path_u, 'PNG')
# 选中状态
img_s = create_icon(icon_name, size, color=COLOR_SELECTED)
path_s = os.path.join(output_dir, f"{icon_name}-active{suffix}.png")
img_s.save(path_s, 'PNG')
print(f" ✓ {os.path.basename(path_u)} ({size}×{size}, {os.path.getsize(path_u)} bytes)")
print(f" ✓ {os.path.basename(path_s)} ({size}×{size}, {os.path.getsize(path_s)} bytes)")
total += 2
print()
print("=" * 50)
print(f"✅ 共生成 {total} 个图标文件")
print(f"📁 保存路径: {output_dir}")
print()
print("文件清单:")
for f in sorted(os.listdir(output_dir)):
if f.endswith('.png'):
filepath = os.path.join(output_dir, f)
print(f" {f} ({os.path.getsize(filepath)} bytes)")
(ROOT / "svg").mkdir(parents=True, exist_ok=True)
for name in TITLES:
for active in (False, True):
state = "-active" if active else ""
(ROOT / "svg" / f"{name}{state}.svg").write_text(svg(name, active), encoding="utf-8")
for size, suffix in ((81, ""), (162, "_2x"), (243, "_3x")):
render(name, size, active).save(ROOT / f"{name}{state}{suffix}.png")
print("已生成 10 个 SVG 和 30 个 PNG 导航图标")
if __name__ == "__main__":