四泳道并行全流水线(资产→开发→测试→玩家迭代闭环),双双 R1-R2 连续达标裁定可交付: - cipher《暗码》223 断言:摩尔斯解码嵌叙事/四结局矩阵/暴露度逐NIGHT验算/三层兜底;R2 三修(链首标记音/预警震/结局计数句+4句补录)4/4 全满分档 - teahouse《深夜茶馆》205 断言:泡茶时机窗/经济逐晚对账/多晚弧线;连续 2 轮 4/4 愿意玩 P0=0 - keeper《守塔人》257 断言:时空签名教学-兑现/插叙触发回返/trust×truth 档位/4结局;R2 F1/F2 坐实(震型豁免/首播跳句锁)4/4 - bridge《星环舰长》336 断言:巡检节奏逐毫秒/危机×系统态矩阵/仲裁7场景/忠诚差分/评级验算;R1 当场修F1-F4、R2 F5 菜单顺延保护 4/4 上行 公共:index.html 四卡片+profiles.js 四桶+check_all_assets 登记(预接线)、00-program 八款进度账收账、 gen_voice_dual 四游戏段、README/HANDOVER 阵容更新(11 款)、voice_text.json 四游戏键、mureka 台账(累计$1.26/$2.00) 验证:四游戏 playtest 223/205/257/336 全绿 + guide 122/122 + 全项目 15 游戏资产终检绿 遗留:keeper 三代人声滤波人耳试听(最高优先,ASR 对滤波句失明)、bridge 音频 32MB 超软指引 2MB(压缩候选)
126 lines
4.5 KiB
Python
126 lines
4.5 KiB
Python
"""web/keeper 封面:1080x1920,孤岛灯塔主题——墨黑风暴海、孤塔双道光束、
|
||
雨丝与闪电微光,竖排标题《守塔人》。可复现脚本(固定 seed,无外部网络依赖)。
|
||
|
||
用法:python3 web/keeper/gen/gen_cover.py
|
||
(或 .venv-tts/bin/python web/keeper/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(2114) # 固定 seed:可复现
|
||
img = Image.new("RGB", (W, H), (6, 10, 18))
|
||
d = ImageDraw.Draw(img)
|
||
|
||
# 背景竖向渐变:墨黑风暴 → 海面微光(灯塔下方最亮)
|
||
for y in range(H):
|
||
t = y / H
|
||
glow = max(0.0, 1.0 - abs(t - 0.62) * 3.2) * 26
|
||
d.line([(0, y), (W, y)],
|
||
fill=(int(6 + 8 * t), int(10 + 12 * t), int(18 + 22 * t + glow)))
|
||
|
||
# 雨丝(风暴签名)
|
||
rain = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
||
rd = ImageDraw.Draw(rain)
|
||
for _ in range(340):
|
||
x = rng.randint(-40, W + 40)
|
||
y = rng.randint(0, H)
|
||
ln = rng.randint(26, 64)
|
||
rd.line([(x, y), (x - ln // 4, y + ln)],
|
||
fill=(150, 175, 205, rng.randint(10, 30)), width=1)
|
||
img.paste(rain, (0, 0), rain)
|
||
d = ImageDraw.Draw(img)
|
||
|
||
# 灯塔(中下偏左):塔身条纹 + 灯室
|
||
cx, base_y = W // 2 - 60, 1250
|
||
top_y = 620
|
||
for i in range(7):
|
||
y0 = top_y + i * 90
|
||
y1 = y0 + 90
|
||
w0 = 46 + i * 7
|
||
w1 = 46 + (i + 1) * 7
|
||
col = (214, 218, 224) if i % 2 == 0 else (158, 42, 43)
|
||
d.polygon([(cx - w0, y0), (cx + w0, y0), (cx + w1, y1), (cx - w1, y1)], fill=col)
|
||
d.rectangle([cx - 40, top_y - 46, cx + 40, top_y], fill=(30, 34, 44))
|
||
d.rectangle([cx - 26, top_y - 38, cx + 26, top_y - 8], fill=(255, 214, 120))
|
||
d.polygon([(cx - 46, top_y - 46), (cx + 46, top_y - 46), (cx, top_y - 84)], fill=(24, 28, 38))
|
||
|
||
# 双道光束(旋转灯扫过雨夜,暖黄、模糊压暗)
|
||
beam = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
||
bd = ImageDraw.Draw(beam)
|
||
for ang, alpha in ((-18, 60), (162, 44)):
|
||
a = math.radians(ang)
|
||
bx, by = cx, top_y - 27
|
||
L = 950
|
||
ex, ey = bx + L * math.cos(a), by - L * math.sin(a) * 0.42
|
||
spread = 90
|
||
perp = (math.cos(a + math.pi / 2), -math.sin(a + math.pi / 2) * 0.42)
|
||
bd.polygon([(bx, by),
|
||
(ex + perp[0] * spread, ey + perp[1] * spread),
|
||
(ex - perp[0] * spread, ey - perp[1] * spread)],
|
||
fill=(255, 206, 110, alpha))
|
||
beam = beam.filter(ImageFilter.GaussianBlur(22))
|
||
img.paste(beam, (0, 0), beam)
|
||
d = ImageDraw.Draw(img)
|
||
|
||
# 海面(塔基下):暗色波纹横线
|
||
sea = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
||
sd = ImageDraw.Draw(sea)
|
||
for i in range(60):
|
||
y = base_y + rng.randint(-20, 330)
|
||
x0 = rng.randint(-60, W - 200)
|
||
ln = rng.randint(120, 400)
|
||
sd.line([(x0, y), (x0 + ln, y)], fill=(90, 120, 150, rng.randint(16, 44)), width=2)
|
||
img.paste(sea, (0, 0), sea)
|
||
d = ImageDraw.Draw(img)
|
||
|
||
# 标题与副题
|
||
f_title = load_font(180)
|
||
f_sub = load_font(52)
|
||
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(1430, "守塔人", f_title, (238, 242, 250), spacing=16)
|
||
ctext(1700, "风暴夜 · 一盏灯 · 一条频道", f_sub, (146, 162, 186))
|
||
|
||
# 手势图例(极简两行)
|
||
hints = ["单击 继续 / 跳句", "左右滑 抉择", "上滑 状态", "下滑 菜单", "双击 重听"]
|
||
for i, s in enumerate(hints):
|
||
col, row = i % 3, i // 3
|
||
d.text((110 + col * 300, 1800 + row * 60), s, font=f_small, fill=(104, 124, 152))
|
||
|
||
ctext(1180, "唱针落下是过去,风声回来是现在。", f_small, (96, 112, 140))
|
||
|
||
img.save(OUT)
|
||
print(OUT, img.size)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|