Files
diary-news/docs/android/assets/_make_logo.py
Mavis 02f0260dfc docs(android): 完整方案 + logo 资源 + 启动屏
新增 docs/android/ 目录:
- README.md 总入口(快速上手 + 决策摘要 + 数据流)
- 01-architecture.md 模块划分 + 数据流 + 选型理由
- 02-api-contract.md 每个接口的请求/响应 + DTO 字段映射
- 03-build-run.md Gradle/SDK/网络安全白名单/真机调试
- 04-milestones.md 7 天里程碑 + DoD + E2E 测试场景

新增 assets/:
- logo/: 主图标 master + adaptive icon + 5 DPI launcher (方/圆)
- splash/: 启动屏 logo + 完整背景预览 + 5 DPI 资源
- android_resources/: 集成所需的 XML(adaptive icon/主题/颜色/字符串/drawable/layout)
- INTEGRATION.md 集成指南
- logo.svg + _make_logo.py 设计源

设计风格:参考用户提供的木质方块字母积木图,米色木纹底 +
深棕色字母 D,代表 'Diary',温暖私人日记感。

服务器体检:所有容器/API/DB/翻译主链路正常,TMT 本月已用 0.37%。
MaaS 备用通道上次已验证可用。
2026-06-10 14:11:43 +08:00

289 lines
10 KiB
Python

"""重做字母 D — 用更干净的 polygon path。"""
from PIL import Image, ImageDraw, ImageFont, ImageChops, ImageFilter
from pathlib import Path
OUT = Path(r'D:\selftools\diary-news\docs\android\assets')
LOGO = OUT / 'logo'
SPLASH = OUT / 'splash'
WOOD_LIGHT = (245, 233, 208)
WOOD_MID = (232, 212, 168)
WOOD_DARK = (201, 168, 118)
GRAIN_LINE = (168, 130, 90)
LETTER_DARK = (62, 42, 30)
LETTER_DARKER = (42, 27, 16)
def lerp(a, b, t):
return tuple(int(a[i] + (b[i] - a[i]) * t) for i in range(3))
def make_wood_gradient(w, h, top, mid, bot):
img = Image.new('RGB', (w, h), top)
px = img.load()
for y in range(h):
t = y / max(1, h - 1)
c = lerp(top, mid, min(1.0, t * 2)) if t < 0.5 else lerp(mid, bot, (t - 0.5) * 2)
for x in range(w):
px[x, y] = c
return img
def add_wood_grain(img, spacing=10, opacity=40):
w, h = img.size
overlay = Image.new('RGBA', (w, h), (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
for y in range(0, h, spacing):
op = max(15, opacity - (y % spacing) * 2)
draw.line([(0, y), (w, y)], fill=(*GRAIN_LINE, op), width=1)
return Image.alpha_composite(img.convert('RGBA'), overlay)
def draw_letter_D_simple(canvas_size, box):
"""画一个干净的 D — 用 polygon 直接画出 D 的外形。
D 的几何:左竖条 + 上下凸出半圆(右半)+ 中间挖空。
"""
img = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
x0, y0, x1, y1 = box
bw, bh = x1 - x0, y1 - y0
cx = (x0 + x1) // 2
cy = (y0 + y1) // 2
# D 的外轮廓尺寸
dw = bw * 0.52
dh = bh * 0.68
L = int(cx - dw / 2) # 左
R = int(cx + dw / 2) # 右
T = int(cy - dh / 2) # 顶
B = int(cy + dh / 2) # 底
# 笔画粗细
bar_w = dw * 0.32
# 凸出(让 D 顶/底圆润)
bulge = bar_w * 0.35
# 1) 画 D 的整体外形:左竖条 + 上下半圆 + 右半椭圆
# 用 polygon 拼一个 D
D_path = [
# 左竖条顶
(L, T + bulge),
# 左竖条底
(L, B - bulge),
# 底弧起点
(L + bar_w * 0.4, B),
# 右半圆底部
(R - bar_w * 0.3, B),
# 右半圆弧最高点(顶部)
(R, cy),
# 右半圆底部 → 已经在 (R - bar_w*0.3, B)
# 顶弧起点
(L + bar_w * 0.4, T),
]
# 实际上我们直接用 pieslice 拼
# 顶半圆:从 (L, T+bulge) 到 (R-bar_w*0.3, T),画 pieslice 180°~360°
# 实际更简单:画三个形状叠起来
# a) 左竖条(矩形)
draw.rectangle(
[L, T + bulge * 0.5, L + bar_w, B - bulge * 0.5],
fill=LETTER_DARK,
)
# b) 顶半圆(右半,从 180° 到 360°,中心点)
# pieslice 接受 bbox + start/end angle(角度,3 点钟方向=0,逆时针为正)
# Pillow 中 pieslice 是顺时针 0=3 点,90=6 点,180=9 点,270=12 点
# 我们要画右上 1/4 圆:从 270° 到 360°(即 12 点 → 3 点)不对
# 重新想:画 D 的右半外轮廓,是一个完整的椭圆右半
# 顶弧:从 (L+bar_w, T) 弧形向右下到 (R, cy)
# 用 arc 描边粗一些,然后用 chord 实心填充
# 直接用 pieslice 实心填充 + ellipse 配合
# 简化方案:
# - 画一个完整 ellipse fill DARK
# - 再画一个稍小的 ellipse fill 木色(挖空内部)
# - 用矩形覆盖椭圆左半,挖出左边的"竖条"
# 这样视觉上就是 D
# 整个 D 占的区域
full_ell = [L, T, R + int(bulge * 0.5), B]
# 让椭圆稍微超出矩形一点,确保右半圆足够圆
inner_ell = [L + bar_w, T + bar_w * 1.05, R + int(bulge * 0.5) - bar_w * 0.55, B - bar_w * 1.05]
# 1) 整个外轮廓 fill DARK
draw.ellipse(full_ell, fill=LETTER_DARK)
# 2) 内部挖空(fill 木色,让字母透出底)
draw.ellipse(inner_ell, fill=WOOD_LIGHT)
# 3) 用矩形盖住椭圆左半,形成 D 的竖条
# 矩形左边到 L+bar_w*0.9,右边到 inner_ell 的左侧+一点
rect_left = L
rect_right = L + bar_w + (inner_ell[0] - (L + bar_w)) // 2 + 2
# 让矩形比椭圆略矮,保持椭圆上下凸出
rect_top = T + bar_w * 0.85
rect_bot = B - bar_w * 0.85
# 矩形 fill DARK(竖条)
draw.rectangle([rect_left, rect_top, rect_right, rect_bot], fill=LETTER_DARK)
# 4) 在矩形右侧挖一个米色矩形,让 D 中间真的空出来
# 计算 D 中间的"肚子"位置
mid_left = L + bar_w + 4
mid_right = R - bar_w * 0.4
mid_top = T + bar_w * 1.1
mid_bot = B - bar_w * 1.1
# 用椭圆 fill 米色 覆盖中间的"空腔"
# 但这样会把竖条也覆盖,改成用 polygon
# 实际上 inner_ell 已经挖空了椭圆内部,现在要把"竖条"也挖掉中间一部分
# 方法:用 ellipse 在竖条右侧挖一个椭圆洞
draw.ellipse(
[mid_left, mid_top, mid_right, mid_bot],
fill=WOOD_LIGHT,
)
# 5) 用矩形 cover 椭圆左半(只留右边空腔)
cover_left = mid_left - 5
cover_right = mid_left + (mid_right - mid_left) * 0.30
draw.rectangle(
[cover_left, mid_top + (mid_bot - mid_top) * 0.05,
cover_right, mid_bot - (mid_bot - mid_top) * 0.05],
fill=WOOD_LIGHT,
)
# 6) 描深色边
# 左竖条外缘
draw.line([(L, T + bulge * 0.5), (L, B - bulge * 0.5)], fill=LETTER_DARKER, width=3)
# 顶弧右端 + 右半圆 + 底弧右端
draw.arc(full_ell, start=270, end=90, fill=LETTER_DARKER, width=3)
return img
def make_block_icon(size, safe_zone=False):
canvas_size = (size, size)
if safe_zone:
pad = int(size * 0.22)
else:
pad = int(size * 0.08)
box = (pad, pad, size - pad, size - pad)
bw, bh = box[2] - box[0], box[3] - box[1]
wood = make_wood_gradient(bw, bh, WOOD_LIGHT, WOOD_MID, WOOD_DARK)
wood = add_wood_grain(wood, spacing=int(size * 0.025), opacity=50)
wood_rgba = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
mask = Image.new('L', (bw, bh), 0)
ImageDraw.Draw(mask).rounded_rectangle([0, 0, bw - 1, bh - 1], radius=int(size * 0.18), fill=255)
wood_rgba.paste(wood, (box[0], box[1]), mask)
edge = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
ed = ImageDraw.Draw(edge)
ed.rounded_rectangle([box[0], box[1], box[2] - 1, box[3] - 1],
radius=int(size * 0.18), outline=(107, 79, 48, 200), width=2)
wood_rgba = Image.alpha_composite(wood_rgba, edge)
shadow = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
sd = ImageDraw.Draw(shadow)
sd.ellipse([int(size * 0.28), int(size * 0.93), int(size * 0.72), int(size * 1.02)], fill=(0, 0, 0, 50))
shadow = shadow.filter(ImageFilter.GaussianBlur(4))
hl = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
hd = ImageDraw.Draw(hl)
hd.rounded_rectangle(
[box[0], box[1], box[2] - 1, box[1] + bh // 2],
radius=int(size * 0.18), fill=(255, 255, 255, 35),
)
letter = draw_letter_D_simple(canvas_size, box)
final = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
final = Image.alpha_composite(final, shadow)
final = Image.alpha_composite(final, wood_rgba)
final = Image.alpha_composite(final, hl)
final = Image.alpha_composite(final, letter)
return final
def make_round_icon(size):
canvas = Image.new('RGBA', (size, size), (0, 0, 0, 0))
wood = make_wood_gradient(size, size, WOOD_LIGHT, WOOD_MID, WOOD_DARK)
wood = add_wood_grain(wood, spacing=int(size * 0.04), opacity=50)
mask = Image.new('L', (size, size), 0)
ImageDraw.Draw(mask).ellipse([0, 0, size - 1, size - 1], fill=255)
canvas.paste(wood, (0, 0), mask)
letter = draw_letter_D_simple(
(size, size),
(int(size * 0.20), int(size * 0.20), int(size * 0.80), int(size * 0.80)),
)
canvas = Image.alpha_composite(canvas, letter)
hl = Image.new('RGBA', (size, size), (0, 0, 0, 0))
hd = ImageDraw.Draw(hl)
hd.ellipse([int(size * 0.1), int(size * 0.05), int(size * 0.9), int(size * 0.55)],
fill=(255, 255, 255, 35))
hl_mask = Image.new('L', (size, size), 0)
ImageDraw.Draw(hl_mask).ellipse([0, 0, size - 1, size - 1], fill=255)
hl.putalpha(ImageChops.multiply(hl.split()[3], hl_mask))
canvas = Image.alpha_composite(canvas, hl)
return canvas
# === 重新生成 ===
print('[1] master 1024')
make_block_icon(1024, safe_zone=False).save(LOGO / 'icon_master_1024.png')
print('[2] adaptive icon 432')
make_block_icon(432, safe_zone=True).save(LOGO / 'ic_launcher_foreground.png')
bg = make_wood_gradient(432, 432, WOOD_LIGHT, WOOD_MID, WOOD_DARK)
bg = add_wood_grain(bg, spacing=12, opacity=30)
bg.save(LOGO / 'ic_launcher_background.png')
print('[3] launcher icons 5 DPI')
SIZES = {'mdpi': 48, 'hdpi': 72, 'xhdpi': 96, 'xxhdpi': 144, 'xxxhdpi': 192}
icon_master = make_block_icon(1024, safe_zone=False)
for dpi, sz in SIZES.items():
d = LOGO / f'mipmap-{dpi}'
d.mkdir(exist_ok=True)
icon_master.resize((sz, sz), Image.LANCZOS).save(d / 'ic_launcher.png')
make_round_icon(sz).save(d / 'ic_launcher_round.png')
print(f' {dpi}: {sz}x{sz}')
print('[4] 启动屏 logo 512')
make_block_icon(512, safe_zone=False).save(SPLASH / 'splash_logo.png')
print('[5] 启动屏背景 1080x1920')
bg_w, bg_h = 1080, 1920
bg_full = make_wood_gradient(bg_w, bg_h, WOOD_LIGHT, WOOD_MID, WOOD_DARK)
bg_full = add_wood_grain(bg_full, spacing=20, opacity=35).convert('RGBA')
logo_big = make_block_icon(512, safe_zone=False)
logo_x = (bg_w - 512) // 2
logo_y = (bg_h - 512) // 2 - 100
bg_full.alpha_composite(logo_big, (logo_x, logo_y))
draw = ImageDraw.Draw(bg_full)
try:
font_big = ImageFont.truetype('arial.ttf', 110)
font_sm = ImageFont.truetype('arial.ttf', 38)
except Exception:
font_big = ImageFont.load_default()
font_sm = ImageFont.load_default()
name = 'Diary News'
bb = draw.textbbox((0, 0), name, font=font_big)
tw = bb[2] - bb[0]
draw.text(((bg_w - tw) // 2, logo_y + 512 + 80), name, fill=LETTER_DARK, font=font_big)
sub = 'Your Private News Diary'
bb2 = draw.textbbox((0, 0), sub, font=font_sm)
tw2 = bb2[2] - bb2[0]
draw.text(((bg_w - tw2) // 2, logo_y + 512 + 230), sub, fill=(90, 65, 40), font=font_sm)
bg_full.save(SPLASH / 'splash_bg_full.png')
print(' OK')
print('[6] 启动屏各 DPI logo')
SPLASH_SIZES = {'mdpi': 192, 'hdpi': 288, 'xhdpi': 384, 'xxhdpi': 576, 'xxxhdpi': 768}
for dpi, sz in SPLASH_SIZES.items():
d = SPLASH / f'drawable-{dpi}'
d.mkdir(exist_ok=True, parents=True)
make_block_icon(sz, safe_zone=False).save(d / 'ic_splash_logo.png')
print(f' {dpi}: {sz}x{sz}')
print('\n=== 完成 ===')