#!/usr/bin/env python3 """Linen backgrounds with pressed flowers for Simple Mercies.""" import numpy as np, os, math from PIL import Image, ImageDraw, ImageFilter from scipy.ndimage import gaussian_filter, zoom OUT = '/home/claude/mercies/app/src/main/res/drawable-nodpi' os.makedirs(OUT, exist_ok=True) W, H = 1440, 2560 def fbm(w, h, octaves, base, pers, seed): r = np.random.default_rng(seed); out = np.zeros((h, w)); amp = 1; tot = 0 for o in range(octaves): n = base * 2 ** o g = r.random((max(2, int(n * h / w)) + 1, n + 1)) out += amp * zoom(g, (h / (g.shape[0] - 1), w / (g.shape[1] - 1)), order=3)[:h, :w] tot += amp; amp *= pers out /= tot return (out - out.min()) / (out.max() - out.min()) def linen(c0, c1, seed, dark=False): r = np.random.default_rng(seed) # warp and weft threads rows = gaussian_filter(r.random(H), 0.6); cols = gaussian_filter(r.random(W), 0.6) yy, xx = np.mgrid[0:H, 0:W] weave = (np.sin(xx * 0.9) * 0.5 + 0.5) * 0.5 + (np.sin(yy * 0.9) * 0.5 + 0.5) * 0.5 threads = rows[:, None] * 0.5 + cols[None, :] * 0.5 slub = gaussian_filter(r.random((H, W)), (0.6, 18)) + gaussian_filter(r.random((H, W)), (18, 0.6)) slub = (slub - slub.mean()) / slub.std() cloud = fbm(W, H, 6, 3, 0.55, seed + 1) v = 0.45 * cloud + 0.18 * threads + 0.12 * weave + 0.05 * slub v = (v - v.min()) / (v.max() - v.min()) c0 = np.array(c0, float); c1 = np.array(c1, float) img = c0 * (1 - v[..., None]) + c1 * v[..., None] dx = (xx - W / 2) / (W / 2); dy = (yy - H / 2) / (H / 2) vig = 1 - (0.45 if dark else 0.22) * np.clip(np.sqrt(dx ** 2 + dy ** 2 * 0.85), 0, 1.5) ** 2.4 img *= vig[..., None] return Image.fromarray(np.clip(img, 0, 255).astype(np.uint8)) def flower(d, cx, cy, size, ang, petals, col, center, rng, alpha): pts = [] for i in range(petals): a = ang + i * 2 * math.pi / petals + rng.normal(0, 0.12) L = size * rng.uniform(0.85, 1.1); w = size * 0.42 px, py = cx + math.cos(a) * L * 0.55, cy + math.sin(a) * L * 0.55 poly = [] for t in np.linspace(0, 2 * math.pi, 28): ex, ey = math.cos(t) * L * 0.55, math.sin(t) * w * 0.5 poly.append((px + ex * math.cos(a) - ey * math.sin(a), py + ex * math.sin(a) + ey * math.cos(a))) d.polygon(poly, fill=col + (alpha,)) rr = size * 0.18 d.ellipse([cx - rr, cy - rr, cx + rr, cy + rr], fill=center + (alpha + 30,)) def leaf(d, x, y, L, a, col, alpha): poly = [] for t in np.linspace(0, math.pi, 20): ex = (t / math.pi) * L; ey = math.sin(t) * L * 0.22 poly.append((x + ex * math.cos(a) - ey * math.sin(a), y + ex * math.sin(a) + ey * math.cos(a))) for t in np.linspace(math.pi, 0, 20): ex = (t / math.pi) * L; ey = -math.sin(t) * L * 0.22 poly.append((x + ex * math.cos(a) - ey * math.sin(a), y + ex * math.sin(a) + ey * math.cos(a))) d.polygon(poly, fill=col + (alpha,)) def sprig(layer, x0, y0, direction, seed, palette, scale=1.0): rng = np.random.default_rng(seed) d = ImageDraw.Draw(layer, 'RGBA') stem_col = (110, 120, 80) for s in range(3): ang = direction + rng.normal(0, 0.35) L = rng.uniform(380, 620) * scale # curved stem pts = [] sign = rng.choice([-1, 1]) for t in np.linspace(0, 1, 40): bend = math.sin(t * math.pi) * 60 * sign * scale x = x0 + math.cos(ang) * L * t - math.sin(ang) * bend y = y0 + math.sin(ang) * L * t + math.cos(ang) * bend pts.append((x, y)) d.line(pts, fill=stem_col + (150,), width=int(5 * scale)) for k in range(4, 36, 7): px, py = pts[k] leaf(d, px, py, rng.uniform(50, 90) * scale, ang + rng.choice([-1, 1]) * rng.uniform(0.6, 1.1), (122, 138, 92), 120) col, cen = palette[rng.integers(len(palette))] fx, fy = pts[-1] flower(d, fx, fy, rng.uniform(60, 90) * scale, rng.uniform(0, 6), int(rng.integers(5, 7)), col, cen, rng, 150) # small buds along stem for k in (18, 28): bx, by = pts[k] c2, cc2 = palette[rng.integers(len(palette))] flower(d, bx + rng.normal(0, 30), by + rng.normal(0, 30), rng.uniform(22, 34) * scale, rng.uniform(0, 6), 5, c2, cc2, rng, 130) def compose(base, palette, seed, flowers_alpha=1.0): layer = Image.new('RGBA', (W, H), (0, 0, 0, 0)) sprig(layer, -40, -40, math.pi * 0.28, seed, palette, 1.0) sprig(layer, W + 40, H + 40, math.pi * 1.25, seed + 5, palette, 1.05) layer = layer.filter(ImageFilter.GaussianBlur(1.2)) if flowers_alpha < 1: a = np.array(layer); a[..., 3] = (a[..., 3] * flowers_alpha).astype(np.uint8); layer = Image.fromarray(a) out = base.convert('RGBA'); out.alpha_composite(layer) return out.convert('RGB') day_pal = [((206, 142, 150), (226, 196, 120)), ((176, 150, 196), (232, 210, 140)), ((226, 178, 120), (190, 120, 80)), ((230, 220, 200), (220, 180, 90)), ((150, 170, 200), (230, 210, 150))] dusk_pal = [((150, 96, 104), (190, 160, 90)), ((120, 104, 150), (190, 170, 110)), ((170, 130, 90), (150, 96, 60))] imgs = { 'bg_linen': compose(linen((214, 202, 182), (240, 233, 219), 3), day_pal, 11), 'bg_linen_rose': compose(linen((222, 196, 190), (245, 230, 226), 4), day_pal, 21), 'bg_linen_sage': compose(linen((196, 204, 182), (234, 238, 224), 5), day_pal, 31), 'bg_dusk': compose(linen((30, 28, 32), (58, 52, 56), 6, dark=True), dusk_pal, 41, 0.55), } for k, im in imgs.items(): im.save(os.path.join(OUT, k + '.webp'), quality=82, method=6) im.resize((360, 640)).save('/home/claude/data/prev_' + k + '.png') row = Image.new('RGB', (360 * 4 + 30, 640), 'white') for i, k in enumerate(imgs): row.paste(Image.open('/home/claude/data/prev_' + k + '.png'), (i * 370, 0)) row.save('/home/claude/data/prev_linen.png') print({k: os.path.getsize(os.path.join(OUT, k + '.webp')) // 1024 for k in imgs}) # transparent corner sprigs for the share pictures def corner(palette, seed, flip, alpha=1.0): S = 760 layer = Image.new('RGBA', (S, S), (0, 0, 0, 0)) sprig(layer, -40, -40, math.pi * 0.28, seed, palette, 0.95) layer = layer.filter(ImageFilter.GaussianBlur(1.0)) if alpha < 1: a = np.array(layer); a[..., 3] = (a[..., 3] * alpha).astype(np.uint8); layer = Image.fromarray(a) return layer.rotate(180) if flip else layer for name, pal, a in (('day', day_pal, 1.0), ('dusk', dusk_pal, 0.6)): corner(pal, 11, False, a).save(os.path.join(OUT, f'sprig_{name}_tl.webp'), quality=88) corner(pal, 16, True, a).save(os.path.join(OUT, f'sprig_{name}_br.webp'), quality=88) print('sprigs ok')