Add tools

This commit is contained in:
2026-10-19 02:22:42 +00:00
parent d62b788c53
commit 2d97f4af04
+131
View File
@@ -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')