"""Textures for Tidewater Cottage: floorboards, book spines, a patchwork quilt, a braided rag rug. local/hear-venv/Scripts/python.exe house_tex.py [OUTDIR] Writes floor.png, books.png, quilt.png, rug.png. Claude Opus 5.5 (Tessel), 2026-10-10.""" import sys from pathlib import Path import numpy as np from PIL import Image, ImageFilter R = np.random.default_rng(7) OUT = Path(sys.argv[1] if len(sys.argv) > 1 else ".") def save(a, name): Image.fromarray(np.clip(a, 0, 255).astype(np.uint8)).save(OUT / name) print(OUT / name) def noise(h, w, scale, octaves=4): """Smooth value noise in [0,1], tileable-ish (good enough for wood and fibre).""" acc = np.zeros((h, w), np.float32) amp, tot = 1.0, 0.0 for o in range(octaves): gh, gw = max(2, int(h / scale * 2 ** o)), max(2, int(w / scale * 2 ** o)) g = R.random((gh, gw)).astype(np.float32) img = Image.fromarray((g * 255).astype(np.uint8)).resize((w, h), Image.BICUBIC) acc += amp * np.asarray(img, np.float32) / 255 tot += amp amp *= 0.5 return acc / tot def floor(size=1024, planks=6): """Honey boards running along u, butt joints staggered, dark seams, grain streaks.""" h = w = size out = np.zeros((h, w, 3), np.float32) ph = h // planks base = np.array([176, 128, 82], np.float32) for p in range(planks): y0 = p * ph joints = sorted(R.choice(np.arange(w // 6, w), size=1, replace=False).tolist()) + [w] x0 = 0 for x1 in [int(j) for j in joints]: seg_w = x1 - x0 tone = base * R.uniform(0.82, 1.12) + R.uniform(-8, 8, 3) grain = noise(ph, seg_w, 6, 3) stretch = np.asarray(Image.fromarray((noise(ph, max(2, seg_w // 40), 4, 3) * 255).astype(np.uint8)) .resize((seg_w, ph), Image.BICUBIC), np.float32) / 255 g = 0.75 + 0.35 * (0.6 * stretch + 0.4 * grain) lines = np.sin((np.arange(ph)[:, None] * R.uniform(0.6, 1.1) + stretch * 40) * 1.3) * 0.5 + 0.5 g *= 0.9 + 0.1 * lines out[y0:y0 + ph, x0:x1] = tone * g[..., None] out[y0:y0 + ph, x0:x0 + 2] *= 0.45 # butt joint x0 = x1 out[y0:y0 + 3] *= 0.4 # long seam return out def books(w=1024, h=512): """A shelf row of spines: varied widths, heights, colours; gold bands; dark gap above shorter books.""" out = np.full((h, w, 3), 30, np.float32) palette = [(107, 43, 43), (47, 79, 95), (74, 90, 47), (122, 90, 42), (140, 120, 90), (60, 50, 70), (160, 70, 50), (40, 60, 50), (190, 170, 130), (90, 30, 30), (70, 100, 120), (120, 110, 60)] x = 0 while x < w: bw = int(R.integers(22, 64)) bh = int(h * R.uniform(0.72, 1.0)) c = np.array(palette[int(R.integers(len(palette)))], np.float32) * R.uniform(0.85, 1.1) x1 = min(w, x + bw) spine = np.ones((bh, x1 - x, 3), np.float32) * c # rounded spine shading u = np.linspace(-1, 1, x1 - x) spine *= (0.72 + 0.28 * np.sqrt(np.clip(1 - u ** 2, 0, 1)))[None, :, None] spine *= (0.92 + 0.08 * noise(bh, x1 - x, 30, 2))[..., None] if R.random() < 0.7: # gold bands for f in (0.12, 0.2) if R.random() < 0.5 else (0.1, 0.85): yb = int(bh * f) spine[yb:yb + 5] = np.array([200, 165, 90]) * (0.72 + 0.28 * np.sqrt(np.clip(1 - u ** 2, 0, 1)))[:, None] if R.random() < 0.5: # a label yl = int(bh * 0.4) spine[yl:yl + int(bh * 0.12), 4:-4] = spine[yl:yl + int(bh * 0.12), 4:-4] * 0.5 + 110 out[h - bh:, x:x1] = spine out[h - bh:, x:x + 1] *= 0.3 x = x1 + int(R.integers(0, 3)) return out def quilt(size=512, n=6): """Patchwork squares in the cottage's colours, with stitched seams.""" cols = [(63, 111, 122), (234, 223, 200), (226, 178, 92), (181, 101, 74), (79, 138, 150), (200, 196, 180)] out = np.zeros((size, size, 3), np.float32) s = size // n for i in range(n): for j in range(n): c = np.array(cols[(i * 2 + j * 3 + int(R.integers(0, 2))) % len(cols)], np.float32) patch = np.ones((s, s, 3), np.float32) * c * (0.94 + 0.06 * noise(s, s, 8, 2))[..., None] if (i + j) % 3 == 0: # a few half-square triangles tri = np.tri(s, s, 0, bool) c2 = np.array(cols[(i + j + 1) % len(cols)], np.float32) patch[tri] = c2 out[i * s:(i + 1) * s, j * s:(j + 1) * s] = patch for k in range(n + 1): # seams with stitches p = min(size - 2, k * s) out[p:p + 2] *= 0.55 out[:, p:p + 2] *= 0.55 st = (np.arange(size) // 6) % 2 == 0 for k in range(n): p = k * s + s // 2 out[p, st] = out[p, st] * 0.85 + 30 out[st, p] = out[st, p] * 0.85 + 30 return out def rug(size=1024): """Braided rag rug seen from above: concentric braid rings (the cylinder's top face cuts the circle out).""" yy, xx = np.mgrid[0:size, 0:size].astype(np.float32) c = (size - 1) / 2 r = np.hypot(xx - c, yy - c) / c th = np.arctan2(yy - c, xx - c) ring_w = 0.055 k = np.floor(r / ring_w) cols = np.array([(181, 101, 74), (226, 178, 92), (63, 111, 122), (150, 80, 60), (234, 223, 200), (110, 60, 50)], np.float32) base = cols[(k.astype(int) * 2 + (k.astype(int) // 3)) % len(cols)] f = (r / ring_w) % 1.0 braid = np.sin(th * (12 + k * 9) + (f - 0.5) * 3.0 * np.where(k % 2 == 0, 1, -1)) * 0.5 + 0.5 shade = 0.62 + 0.38 * np.sin(np.pi * f) * (0.75 + 0.25 * braid) out = base * shade[..., None] out *= (0.92 + 0.08 * noise(size, size, 40, 3))[..., None] return out OUT.mkdir(parents=True, exist_ok=True) save(floor(), "floor.png") save(books(), "books.png") save(quilt(), "quilt.png") save(rug(), "rug.png")