Files
Nicholas ButzkeandClaude Fable 5 648872a16d feat: ad-graphic layer — generated posters, billboards and sign columns
The city's flat color sign panels are now real graphic designs, closing
the street-level "graphic density" that anime cities live on:

- tools/generate_ads.py: procedural ad library (24 textures) — bold
  two-tone layouts (diagonal splits, bands, circles, stripes) with
  fake-glyph text blocks that read as kanji/kana from gameplay distance,
  inked borders, seeded palette system
- Every building gets 1-3 street-level posters plastered on its facade
- Vertical sign columns render a stacked-glyph column texture
- Rooftop billboards, tower crown boards (both faces) and the central
  4-face screen tower all show distinct ad graphics
- Posters under the viaduct arches on every pier
- All panels lit (albedo + emission of the same texture) so they glow
  as signage without blooming out

Verified via street capture (glyph posters, pharmacy-cross signs, ad
boards visible down the avenue); movement 11/11, smoke 0 failures,
acoustics ALL PASSED.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 02:44:55 -04:00

126 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""Generate stylized ad/poster/sign textures for Akiba Crossing.
Bold two-tone layouts, punchy shapes and fake-glyph text blocks (random
stroke clusters that read as kanji/kana from gameplay distance) — the
graphic-design density of an anime city street, generated as a reusable
library:
assets/textures/ads/ad_XX.png (vertical 256x512 posters/banners)
assets/textures/ads/board_XX.png (horizontal 512x256 billboards)
assets/textures/ads/column_XX.png (128x512 vertical sign columns)
Run: python tools/generate_ads.py
"""
import random
from pathlib import Path
from PIL import Image, ImageDraw
OUT = Path("assets/textures/ads")
OUT.mkdir(parents=True, exist_ok=True)
PALETTES = [
[(238, 51, 94), (255, 255, 255), (24, 24, 32)],
[(38, 206, 220), (24, 24, 40), (255, 255, 255)],
[(255, 196, 37), (30, 30, 34), (238, 51, 94)],
[(120, 90, 240), (255, 255, 255), (255, 196, 37)],
[(46, 230, 120), (18, 24, 24), (255, 255, 255)],
[(255, 255, 255), (238, 51, 94), (24, 24, 32)],
[(24, 28, 44), (38, 206, 220), (255, 196, 37)],
[(255, 120, 40), (255, 255, 255), (30, 30, 34)],
]
rng = random.Random(20260719)
def glyph(d: ImageDraw.ImageDraw, x, y, s, color):
"""A fake kanji-ish glyph: a few thick strokes in an s x s box."""
w = max(2, s // 7)
strokes = rng.randint(2, 4)
for _ in range(strokes):
kind = rng.randint(0, 2)
if kind == 0: # horizontal bar
yy = y + rng.randint(0, s - w)
d.rectangle([x, yy, x + s, yy + w], fill=color)
elif kind == 1: # vertical bar
xx = x + rng.randint(0, s - w)
d.rectangle([xx, y, xx + w, y + s], fill=color)
else: # box
m = rng.randint(s // 5, s // 3)
d.rectangle([x + m, y + m, x + s - m, y + s - m], outline=color, width=w)
def text_block(d, x, y, s, count, color, vertical=True):
for i in range(count):
if vertical:
glyph(d, x, y + i * int(s * 1.25), s, color)
else:
glyph(d, x + i * int(s * 1.25), y, s, color)
def background(d, w, h, pal):
d.rectangle([0, 0, w, h], fill=pal[0])
layout = rng.randint(0, 3)
if layout == 0: # diagonal split
d.polygon([(0, h), (w, h), (w, rng.randint(0, h // 2))], fill=pal[1])
elif layout == 1: # bottom band
d.rectangle([0, int(h * 0.72), w, h], fill=pal[1])
elif layout == 2: # big circle
r = int(min(w, h) * 0.42)
cx, cy = rng.randint(r, w - r), rng.randint(r, h - r)
d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=pal[1])
else: # stripes
n = rng.choice([3, 4])
for i in range(n):
if i % 2 == 0:
d.rectangle([i * w // n, 0, (i + 1) * w // n, h], fill=pal[1])
def poster(path, w, h):
pal = rng.choice(PALETTES)
img = Image.new("RGB", (w, h))
d = ImageDraw.Draw(img)
background(d, w, h, pal)
ink = pal[2]
if h > w: # vertical: glyph column + accent dot
s = w // 4
text_block(d, w - s - s // 2, s // 2, s, rng.randint(3, 5), ink, vertical=True)
if rng.random() < 0.6:
r = w // 6
d.ellipse([s // 2, h - 2 * r - s // 2, s // 2 + 2 * r, h - s // 2], fill=ink)
else: # horizontal: glyph row
s = h // 3
text_block(d, s // 2, (h - s) // 2, s, rng.randint(3, 6), ink, vertical=False)
# border
d.rectangle([0, 0, w - 1, h - 1], outline=(15, 14, 20), width=max(3, w // 60))
img.save(path)
def column(path, w, h):
"""Vertical sign column: stacked glyphs, one per segment."""
pal = rng.choice(PALETTES)
img = Image.new("RGB", (w, h))
d = ImageDraw.Draw(img)
d.rectangle([0, 0, w, h], fill=pal[0])
s = int(w * 0.6)
n = h // int(s * 1.4)
for i in range(n):
glyph(d, (w - s) // 2, int(i * s * 1.4 + s * 0.25), s, pal[2])
d.rectangle([0, 0, w - 1, h - 1], outline=(15, 14, 20), width=3)
img.save(path)
def main():
for i in range(10):
poster(OUT / f"ad_{i:02d}.png", 256, 512)
for i in range(8):
poster(OUT / f"board_{i:02d}.png", 512, 256)
for i in range(6):
column(OUT / f"column_{i:02d}.png", 128, 512)
print(f"generated {10 + 8 + 6} ad textures in {OUT}")
if __name__ == "__main__":
main()