"""web/detective 封面:1080x1920,雨夜推理主题——墨蓝夜雨、一盏路灯暖光、 雨丝竖线与居中标题《雨夜专案》。可复现脚本(固定 seed,无外部网络依赖)。 用法(PIL 探测顺序:系统 python3 → .venv-tts → 装 pillow 后任一): python3 web/detective/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(918) # 固定 seed:可复现 img = Image.new("RGB", (W, H), (10, 12, 20)) d = ImageDraw.Draw(img) # 背景竖向渐变:夜空墨蓝 -> 街面冷灰(雨夜街道) for y in range(H): t = y / H d.line([(0, y), (W, y)], fill=(int(14 + 10 * t), int(17 + 12 * t), int(26 + 18 * t))) # 一盏路灯暖光(左上,光晕压暗)——雨夜里唯一的暖色 lx, ly = 250, 380 lamp = Image.new("RGBA", (W, H), (0, 0, 0, 0)) ld = ImageDraw.Draw(lamp) for r, alpha in [(420, 26), (300, 40), (190, 58), (110, 80)]: ld.ellipse([lx - r, ly - r, lx + r, ly + r], fill=(253, 230, 138, alpha)) lamp = lamp.filter(ImageFilter.GaussianBlur(14)) img.paste(lamp, (0, 0), lamp) d = ImageDraw.Draw(img) d.ellipse([lx - 26, ly - 26, lx + 26, ly + 26], fill=(255, 244, 214)) # 雨丝:斜细线两景深(远淡近亮),风斜角 ~8° rain = Image.new("RGBA", (W, H), (0, 0, 0, 0)) rd = ImageDraw.Draw(rain) for _ in range(90): # 远景雨 x = rng.randint(0, W + 200) y = rng.randint(0, H) ln = rng.randint(40, 90) rd.line([(x, y), (x - ln * 0.14, y + ln)], fill=(148, 163, 184, rng.randint(16, 34)), width=1) for _ in range(60): # 近景雨 x = rng.randint(0, W + 200) y = rng.randint(0, H) ln = rng.randint(90, 160) rd.line([(x, y), (x - ln * 0.14, y + ln)], fill=(203, 213, 225, rng.randint(28, 52)), width=2) img.paste(rain, (0, 0), rain) d = ImageDraw.Draw(img) # 中央:案卷卷宗剪影(翻开的两页,暗金色边线)——「卷宗六份」的具象 cx, cy = W // 2, 640 doc = Image.new("RGBA", (W, H), (0, 0, 0, 0)) ddoc = ImageDraw.Draw(doc) ddoc.polygon([(cx - 40, cy - 120), (cx - 250, cy - 60), (cx - 250, cy + 190), (cx - 40, cy + 130)], outline=(120, 113, 108, 220), width=4) ddoc.polygon([(cx + 40, cy - 120), (cx + 250, cy - 60), (cx + 250, cy + 190), (cx + 40, cy + 130)], outline=(120, 113, 108, 220), width=4) # 页面行文线(证词的横线,右页夹一条对不上的红线) for i in range(5): yy = cy - 26 + i * 40 ddoc.line([(cx - 215, yy - 28), (cx - 75, yy - 46)], fill=(148, 163, 184, 120), width=3) ddoc.line([(cx + 75, yy - 46), (cx + 215, yy - 28)], fill=(148, 163, 184, 120), width=3) ddoc.line([(cx + 92, cy + 62), (cx + 208, cy + 80)], fill=(248, 113, 113, 200), width=4) img.paste(doc, (0, 0), doc) 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(1010, "雨夜专案", f_title, (237, 233, 254), spacing=16) ctext(1262, "证词会撒谎,证据不会", f_sub, (148, 163, 184)) # 手势图例(极简两行) 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=(104, 122, 152)) ctext(1700, "翻听六份卷宗,指出矛盾,锁定真凶。", f_small, (86, 102, 130)) img.save(OUT) print(OUT, img.size) if __name__ == "__main__": main()