Files
2026-10-19 01:19:52 +00:00

128 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Procedural aged backgrounds: vellum, papyrus, parchment, candlelight (night)."""
import numpy as np
from PIL import Image
from scipy.ndimage import gaussian_filter, zoom
import os
OUT = '/home/claude/vellum/app/src/main/res/drawable-nodpi'
os.makedirs(OUT, exist_ok=True)
W, H = 1440, 2560
rng = np.random.default_rng(7)
def fbm(w, h, octaves=6, base=4, persistence=0.55, seed=0):
r = np.random.default_rng(seed)
out = np.zeros((h, w))
amp = 1.0; tot = 0
for o in range(octaves):
n = base * 2 ** o
g = r.random((max(2, int(n * h / w)) + 1, n + 1))
z = zoom(g, (h / (g.shape[0] - 1), w / (g.shape[1] - 1)), order=3)[:h, :w]
out += amp * z; tot += amp; amp *= persistence
out /= tot
out = (out - out.min()) / (out.max() - out.min())
return out
def vignette(w, h, strength=0.5, power=2.2):
y, x = np.mgrid[0:h, 0:w]
dx = (x - w / 2) / (w / 2); dy = (y - h / 2) / (h / 2)
d = np.sqrt(dx ** 2 * 1.0 + dy ** 2 * 0.85)
return 1 - strength * np.clip(d, 0, 1.6) ** power / (1.6 ** power) * 1.6
def colorize(v, c0, c1):
c0 = np.array(c0, float); c1 = np.array(c1, float)
return c0[None, None, :] * (1 - v[..., None]) + c1[None, None, :] * v[..., None]
def save(img, name):
a = np.clip(img, 0, 255).astype(np.uint8)
Image.fromarray(a).save(os.path.join(OUT, name + '.webp'), quality=82, method=6)
Image.fromarray(a).resize((360, 640)).save('/home/claude/data/prev_' + name + '.png')
def grain(w, h, amt, seed):
r = np.random.default_rng(seed)
return (r.random((h, w)) - 0.5) * amt
# ---- Vellum: warm cream, soft mottled clouds, faint hair follicle speckle, darker edges
def vellum():
m = fbm(W, H, 7, 3, 0.58, 1)
m2 = fbm(W, H, 5, 8, 0.5, 2)
base = colorize(m * 0.7 + m2 * 0.3, (222, 200, 160), (246, 234, 206))
# stains
st = fbm(W, H, 4, 2, 0.6, 3)
stain = np.clip((st - 0.62) * 3.5, 0, 1)
base = base * (1 - 0.12 * stain[..., None]) + np.array([150, 110, 60]) * 0.12 * stain[..., None]
# follicle speckle
sp = rng.random((H, W)) > 0.9985
sp = gaussian_filter(sp.astype(float), 1.2) * 6
base -= sp[..., None] * np.array([40, 45, 55])
base += grain(W, H, 10, 4)[..., None]
base *= vignette(W, H, 0.38, 2.6)[..., None]
save(base, 'bg_vellum')
# ---- Papyrus: crossed plant strips, horizontal layer over vertical layer
def papyrus():
r = np.random.default_rng(11)
# vertical strips
def strips(n, length, axis, seed):
rr = np.random.default_rng(seed)
widths = rr.integers(60, 140, n)
prof = []
for wdt in widths:
t = np.linspace(0, 1, wdt)
edge = np.clip(np.minimum(t, 1 - t) * 9, 0, 1)
fibers = gaussian_filter(rr.random(wdt), 0.8)
prof.append(0.75 + 0.25 * edge + (fibers - 0.5) * 0.25 + rr.normal(0, 0.03))
p = np.concatenate(prof)
return p
pv = strips(30, H, 0, 12)[:W]
pv = np.pad(pv, (0, max(0, W - len(pv))), mode='edge')
ph = strips(40, W, 1, 13)[:H]
ph = np.pad(ph, (0, max(0, H - len(ph))), mode='edge')
fv = gaussian_filter(r.random((H, W)), (40, 0.7)) # long vertical fibers
fh = gaussian_filter(r.random((H, W)), (0.7, 40)) # long horizontal fibers
fv = (fv - fv.mean()) / fv.std(); fh = (fh - fh.mean()) / fh.std()
v = 0.55 * ph[:, None] * np.ones((1, W)) + 0.25 * pv[None, :] * np.ones((H, 1))
v += 0.05 * fh + 0.025 * fv
blot = fbm(W, H, 6, 3, 0.55, 14)
v = v * 0.8 + blot * 0.25
v = (v - v.min()) / (v.max() - v.min())
img = colorize(v, (204, 174, 120), (244, 228, 188))
dark = np.clip((fbm(W, H, 4, 2, 0.6, 15) - 0.66) * 3, 0, 1)
img *= (1 - 0.10 * dark)[..., None]
img += grain(W, H, 12, 16)[..., None]
img *= vignette(W, H, 0.42, 2.4)[..., None]
save(img, 'bg_papyrus')
# ---- Parchment: older, darker, more blotchy with burnt edges
def parchment():
m = fbm(W, H, 7, 3, 0.62, 21)
img = colorize(m, (196, 160, 104), (238, 214, 168))
bl = fbm(W, H, 5, 5, 0.55, 22)
b = np.clip((bl - 0.58) * 4, 0, 1)
img = img * (1 - 0.22 * b[..., None]) + np.array([120, 80, 35]) * 0.22 * b[..., None]
y, x = np.mgrid[0:H, 0:W]
edge = np.minimum.reduce([x, W - 1 - x, y, H - 1 - y]).astype(float)
wob = fbm(W, H, 5, 6, 0.5, 23) * 70
e = np.clip(1 - (edge - wob) / 120, 0, 1) ** 1.6
img = img * (1 - 0.65 * e[..., None]) + np.array([70, 40, 15]) * 0.65 * e[..., None]
img += grain(W, H, 12, 24)[..., None]
img *= vignette(W, H, 0.3, 2.2)[..., None]
save(img, 'bg_parchment')
# ---- Candlelight night: deep umber parchment, warm glow from lower centre
def candle():
m = fbm(W, H, 7, 3, 0.6, 31)
img = colorize(m, (22, 14, 8), (52, 34, 18))
y, x = np.mgrid[0:H, 0:W]
d = np.sqrt(((x - W * 0.5) / W) ** 2 + ((y - H * 0.78) / H) ** 2 * 1.4)
glow = np.exp(-(d / 0.42) ** 2)
img += glow[..., None] * np.array([70, 40, 12])
img += grain(W, H, 6, 32)[..., None]
img *= vignette(W, H, 0.55, 2.0)[..., None]
save(img, 'bg_candle')
if __name__ == '__main__':
vellum(); papyrus(); parchment(); candle()
for f in sorted(os.listdir(OUT)):
print(f, os.path.getsize(os.path.join(OUT, f)))