diff --git a/tools/linen.py b/tools/linen.py new file mode 100644 index 0000000..26d3da4 --- /dev/null +++ b/tools/linen.py @@ -0,0 +1,131 @@ +#!/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')