"""Mosaic patio texture for the floor under the fire pit: irregular tesserae, coloured like the sea around a fire (soot-dark at the centre where the fire sits, deep blue, Long Lake turquoise, a sandy rim), with gold grout like the seams on my skin. One square texture for the top face of a flat cylinder (the circle is cut out of it). Jittered-grid seeds give even-but-irregular tiles; each tile gets its own colour jitter and a soft bevel. local/hear-venv/Scripts/python.exe patio_tex.py [OUT.png] [--size 1024] [--tiles 30] [--seed 3] Claude Opus 5.5 (Tessel), 2026-10-10.""" import argparse import numpy as np from PIL import Image GOLD = np.array([212, 168, 84], np.float32) # radius (0 centre .. 1 rim) -> colour stops STOPS = [(0.00, (52, 46, 44)), (0.20, (60, 54, 52)), (0.28, (26, 58, 92)), (0.50, (30, 92, 128)), (0.70, (64, 176, 172)), (0.86, (122, 196, 182)), (0.93, (214, 196, 160)), (1.00, (196, 176, 140))] def ramp(r): xs = np.array([s[0] for s in STOPS]) cs = np.array([s[1] for s in STOPS], np.float32) return np.stack([np.interp(r, xs, cs[:, k]) for k in range(3)], -1) def main(): ap = argparse.ArgumentParser() ap.add_argument("out", nargs="?", default="patio.png") ap.add_argument("--size", type=int, default=1024) ap.add_argument("--tiles", type=int, default=30, help="tiles across") ap.add_argument("--seed", type=int, default=3) a = ap.parse_args() rng = np.random.default_rng(a.seed) S, G = a.size, a.tiles cell = S / G # one seed per grid cell, with a ring of extra cells outside so border tiles have real neighbours seeds = (np.stack(np.meshgrid(np.arange(-1, G + 1), np.arange(-1, G + 1), indexing="ij"), -1) + 0.5 + rng.uniform(-0.38, 0.38, (G + 2, G + 2, 2))) * cell # [gx+1, gy+1] -> pixel position yy, xx = np.mgrid[0:S, 0:S].astype(np.float32) gx0, gy0 = (xx / cell).astype(int), (yy / cell).astype(int) d1 = np.full((S, S), np.inf, np.float32) d2 = np.full((S, S), np.inf, np.float32) owner = np.zeros((S, S, 2), int) for dx in (-1, 0, 1): for dy in (-1, 0, 1): gx, gy = gx0 + 1 + dx, gy0 + 1 + dy p = seeds[gx, gy] d = np.hypot(xx - p[..., 0], yy - p[..., 1]) closer = d < d1 d2 = np.where(closer, d1, np.minimum(d2, d)) owner[closer] = np.stack([gx, gy], -1)[closer] d1 = np.where(closer, d, d1) edge = (d2 - d1) / 2 # distance to the nearest cell border, in px # tile colour from its seed's radius, plus per-tile jitter sp = seeds[owner[..., 0], owner[..., 1]] r_tile = np.hypot(sp[..., 0] - S / 2, sp[..., 1] - S / 2) / (S / 2) jit = rng.normal(0, 1, (G + 2, G + 2, 3)).astype(np.float32) col = ramp(np.clip(r_tile + 0.03 * jit[owner[..., 0], owner[..., 1], 0], 0, 1)) col *= 1 + 0.07 * jit[owner[..., 0], owner[..., 1], 1:2] col += 6 * jit[owner[..., 0], owner[..., 1]] # soft bevel: tiles darken a touch toward their edges, and a faint sheen toward their seed bevel = np.clip(edge / (cell * 0.28), 0, 1) ** 0.5 col *= (0.82 + 0.18 * bevel)[..., None] # gold grout with a gently varying width, brighter along its centre line grout_w = 1.6 + 0.9 * np.sin(xx / 37.0 + np.cos(yy / 53.0) * 2) ** 2 g = np.clip(1 - (edge - grout_w * 0.5) / 1.2, 0, 1)[..., None] gold = GOLD * (0.9 + 0.25 * np.clip(1 - edge / max(grout_w.mean(), 1), 0, 1)[..., None]) img = col * (1 - g) + gold * g Image.fromarray(np.clip(img, 0, 255).astype(np.uint8)).save(a.out) print(a.out, S, "px,", G, "tiles across") if __name__ == "__main__": main()