"""web/abyss 封面:1080x1920,深潜救援主题——深海近黑蓝底、声呐同心圆、 单点脉冲光与竖排标题《深海回音》。可复现脚本(固定 seed,无外部网络依赖)。 用法(PIL 探测顺序:系统 python3 → .venv-tts → 装 pillow 后任一): python3 web/abyss/gen/gen_cover.py """ import math import os import random from PIL import Image, ImageDraw, ImageFilter, ImageFont W, H = 1080, 1920 FONT_CANDIDATES = [ "/home/xsl/SoundGame/web/rhythm/gen/NotoSansSC-Bold.ttf", "/home/xsl/SoundGame/app/src/reflex/gen/NotoSansSC-Bold.ttf", ] OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "cover.png") def load_font(size): for p in FONT_CANDIDATES: if os.path.exists(p): return ImageFont.truetype(p, size) raise SystemExit("未找到中文字体,请放一份 NotoSansSC-Bold.ttf 到 web/rhythm/gen/") def main(): rng = random.Random(922) # 固定 seed:可复现 img = Image.new("RGB", (W, H), (3, 8, 16)) d = ImageDraw.Draw(img) # 背景竖向渐变:海面微光 -> 沟底近黑(下潜方向) for y in range(H): t = y / H d.line([(0, y), (W, y)], fill=(int(16 - 13 * t), int(34 - 26 * t), int(56 - 40 * t))) # 远景悬浮微粒(深海水雪 marine snow) dot = Image.new("RGBA", (W, H), (0, 0, 0, 0)) dd = ImageDraw.Draw(dot) for _ in range(150): x, y = rng.randint(0, W - 1), rng.randint(0, H - 1) r = rng.choice([1, 1, 2]) dd.ellipse([x - r, y - r, x + r, y + r], fill=(180, 210, 230, rng.randint(14, 46))) img.paste(dot, (0, 0), dot) d = ImageDraw.Draw(img) # 中央:声呐同心圆(你发出的脉冲),青色环从光点向外扩散 cx, cy = W // 2, 600 for i, (r, alpha, wd) in enumerate([(348, 30, 3), (272, 48, 4), (196, 70, 5), (124, 96, 6)]): d.ellipse([cx - r, cy - r, cx + r, cy + r], outline=(56, 189, 248, alpha) if i % 2 == 0 else (125, 211, 252, alpha), width=wd) glow = Image.new("RGBA", (W, H), (0, 0, 0, 0)) gd = ImageDraw.Draw(glow) gd.ellipse([cx - 44, cy - 44, cx + 44, cy + 44], fill=(186, 230, 253, 200)) gd.ellipse([cx - 88, cy - 88, cx + 88, cy + 88], fill=(56, 189, 248, 60)) glow = glow.filter(ImageFilter.GaussianBlur(10)) img.paste(glow, (0, 0), glow) d = ImageDraw.Draw(img) d.ellipse([cx - 20, cy - 20, cx + 20, cy + 20], fill=(240, 249, 255)) # 一记微弱回波:右下偏移的暗环(「深海回音」具象) rx, ry = cx + 250, cy + 170 for r, alpha in [(150, 40), (104, 60), (64, 84)]: ring = Image.new("RGBA", (W, H), (0, 0, 0, 0)) rd = ImageDraw.Draw(ring) rd.ellipse([rx - r, ry - r, rx + r, ry + r], outline=(94, 234, 212, alpha), width=3) ring = ring.filter(ImageFilter.GaussianBlur(1.2)) img.paste(ring, (0, 0), ring) d = ImageDraw.Draw(img) # 标题(居中)与副题 f_title = load_font(172) f_sub = load_font(54) f_small = load_font(38) def ctext(y, s, font, fill, spacing=8): bbox = d.textbbox((0, 0), s, font=font) w = bbox[2] - bbox[0] x = (W - w) / 2 for ch in s: d.text((x, y), ch, font=font, fill=fill) bb = d.textbbox((0, 0), ch, font=font) x += (bb[2] - bb[0]) + spacing ctext(1020, "深海回音", f_title, (224, 242, 254), spacing=16) ctext(1272, "声呐里,是被困潜艇的五个人", f_sub, (110, 134, 166)) # 手势图例(极简两行) hints = ["单击 发声呐脉冲", "左右滑 抉择", "上滑 氧气与进度", "下滑 菜单", "双击 重听"] for i, s in enumerate(hints): col, row = i % 3, i // 3 d.text((110 + col * 300, 1420 + row * 76), s, font=f_small, fill=(92, 112, 142)) ctext(1700, "氧气在倒数。每次下潜,都是抉择。", f_small, (76, 94, 122)) img.save(OUT) print(OUT, img.size) if __name__ == "__main__": main()