新增 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 备用通道上次已验证可用。
289 lines
10 KiB
Python
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=== 完成 ===') |