nomeansnoquest/tools/make_cover.py
m3ultra c43fa2fa18 Cover tooling + deploy: flux base art, Pillow title stamp + riso duotone, MBW/MBH dims
Live at https://monsterrobot.games/games/nomeansnoquest/ (botchat bind mount); card on the landing arc grid. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 14:37:42 +10:00

95 lines
2.9 KiB
Python

#!/usr/bin/env python3
"""Finish the landing-page cover pair from the flux base image.
Stamps the title (so the band name is spelled by us, not by a diffusion
model) and derives the risograph hover variant (two-ink duotone, offset
plates, paper grain). python3 tools/make_cover.py OUT_DIR
"""
import os
import random
import sys
from PIL import Image, ImageDraw, ImageEnhance, ImageFont, ImageOps
HERE = os.path.dirname(__file__)
BASE = os.path.join(HERE, "..", "pics", "cover-blank.png")
FONTS = [
"/System/Library/Fonts/Supplemental/Impact.ttf",
"/System/Library/Fonts/Supplemental/Arial Black.ttf",
"/System/Library/Fonts/Helvetica.ttc",
]
def font(size):
for f in FONTS:
if os.path.exists(f):
try:
return ImageFont.truetype(f, size)
except OSError:
continue
return ImageFont.load_default()
def stamp_title(img):
d = ImageDraw.Draw(img)
w = img.width
def line(text, y, size, fill):
f = font(size)
tw = d.textlength(text, font=f)
x = (w - tw) / 2
# fat xerox outline: many offset passes
for dx in range(-4, 5, 2):
for dy in range(-4, 5, 2):
d.text((x + dx, y + dy), text, font=f, fill=(12, 10, 8))
d.text((x, y), text, font=f, fill=fill)
line("NOMEANSNO", 26, 118, (238, 232, 220))
line("QUEST", 148, 96, (208, 40, 32))
return img
def riso(img):
paper = Image.new("RGB", img.size, (240, 233, 216))
g = ImageOps.autocontrast(img.convert("L"))
# ink plates from tone bands
dark = g.point(lambda v: 255 if v < 90 else 0)
mid = g.point(lambda v: 255 if 90 <= v < 175 else 0)
blue = Image.new("RGB", img.size, (36, 62, 168))
red = Image.new("RGB", img.size, (222, 60, 44))
out = paper.copy()
# misregistered plates, like a real duplicator on a deadline
out.paste(red, (3, 2), mid)
out.paste(blue, (0, 0), dark)
# grain
rng = random.Random(41)
px = out.load()
for _ in range(img.width * img.height // 14):
x = rng.randrange(img.width)
y = rng.randrange(img.height)
r, gr, b = px[x, y]
n = rng.randint(-18, 18)
px[x, y] = (max(0, min(255, r + n)), max(0, min(255, gr + n)), max(0, min(255, b + n)))
return out
def main():
out_dir = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "..", "covers")
os.makedirs(out_dir, exist_ok=True)
base = Image.open(BASE).convert("RGB")
base = ImageEnhance.Contrast(base).enhance(1.08)
titled = stamp_title(base.copy())
cover = ImageOps.fit(titled, (311, 400), Image.LANCZOS)
cover.save(os.path.join(out_dir, "nomeansnoquest.jpg"), quality=88)
riso_img = ImageOps.fit(riso(stamp_title(base.copy())), (311, 400), Image.LANCZOS)
riso_img.save(os.path.join(out_dir, "nomeansnoquest_riso.jpg"), quality=88)
print("covers written:", out_dir)
if __name__ == "__main__":
main()