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]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
56db9eea59
commit
648872a16d
@@ -0,0 +1,125 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user