"""web/cipher 封面:1080x1920,1940 密战主题——墨绿电波底、摩尔斯点划 电文、耳机轮廓微光与竖排标题《暗码》。可复现脚本(固定 seed,无外部网络依赖)。 用法:python3 web/cipher/gen/gen_cover.py (或 .venv-tts/bin/python web/cipher/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(1940) # 固定 seed:可复现 img = Image.new("RGB", (W, H), (10, 16, 14)) d = ImageDraw.Draw(img) # 背景竖向微渐变:军绿墨黑 → 电台暖绿(值机室的电子管夜光) for y in range(H): t = y / H d.line([(0, y), (W, y)], fill=(int(10 + 8 * t), int(16 + 18 * t), int(14 + 12 * t))) # 示波器绿光波纹(密电的频率具象,模糊压暗做背景) wave = Image.new("RGBA", (W, H), (0, 0, 0, 0)) wd = ImageDraw.Draw(wave) for i in range(20): cy = rng.randint(0, H) amp = rng.randint(12, 60) phase = rng.uniform(0, math.pi * 2) freq = rng.uniform(0.004, 0.012) pts = [(x, cy + amp * math.sin(freq * x + phase) * math.exp(-((x - W / 2) / (W * 0.42)) ** 2)) for x in range(0, W, 8)] wd.line(pts, fill=(96, 200, 140, rng.randint(10, 30)), width=3) wave = wave.filter(ImageFilter.GaussianBlur(1.8)) img.paste(wave, (0, 0), wave) d = ImageDraw.Draw(img) # 中央:耳机轮廓 + 声波同心弧(「用耳朵当武器」的具象) cx, cy = W // 2, 660 for i, (r, alpha) in enumerate([(310, 40), (236, 72), (168, 108), (110, 150)]): ring = Image.new("RGBA", (W, H), (0, 0, 0, 0)) rd = ImageDraw.Draw(ring) rd.arc([cx - r, cy - r, cx + r, cy + r], start=200, end=340, fill=(140, 230, 170, alpha), width=9 - i * 2) img.paste(ring, (0, 0), ring) # 头梁 + 两侧耳罩 d.arc([cx - 190, cy - 210, cx + 190, cy + 170], start=180, end=360, fill=(190, 235, 205, 210), width=26) for sx in (-1, 1): d.rounded_rectangle([cx + sx * 205 - 64, cy - 40, cx + sx * 205 + 64, cy + 190], radius=64, fill=(24, 44, 34), outline=(170, 230, 190, 220), width=10) # 电键(手键)剪影:底座 + 黄铜杆 ky = 1080 d.rounded_rectangle([cx - 150, ky + 60, cx + 150, ky + 110], radius=18, fill=(28, 40, 32)) d.line([cx - 30, ky + 62, cx + 96, ky - 46], fill=(212, 178, 106), width=22) d.ellipse([cx + 82, ky - 72, cx + 132, ky - 22], fill=(232, 200, 130)) # 摩尔斯电文点划带:BAY ·− −··· / ·− / −·−− 的意象(不拼真码,做装饰带) band_y = 1290 code = ([3, 1, 1, 1], [1, 3], [1, 3, 1, 3]) # −··· ·− −·−−(B A Y) x = cx - 330 for letter in code: for u in letter: w = 34 if u == 3 else 14 d.rounded_rectangle([x, band_y - 9, x + w, band_y + 9], radius=8, fill=(150, 235, 180, 220)) x += w + 22 x += 34 # 静电噪点 for _ in range(700): x, y = rng.randint(0, W - 1), rng.randint(0, H - 1) a = rng.randint(6, 22) img.putpixel((x, y), (140, 220, 170)) img = img.filter(ImageFilter.GaussianBlur(0.4)) d = ImageDraw.Draw(img) # 竖排标题《暗码》(右下留白区,避免与遮罩文字重叠) f_title = load_font(236) f_sub = load_font(44) tx, ty = W - 330, 1420 for i, ch in enumerate("暗码"): d.text((tx, ty + i * 260), ch, font=f_title, fill=(226, 242, 230), stroke_width=6, stroke_fill=(10, 22, 16)) d.text((tx + 40, ty + 2 * 260 + 30), "1940 · 江城密电", font=f_sub, fill=(150, 210, 175)) img.save(OUT) print("saved", OUT) if __name__ == "__main__": main()