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>
95 lines
2.9 KiB
Python
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()
|