#!/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)))