"""Tidewater: a top dyed like Long Lake's shallows (deep blue at the shoulders, turquoise at the hem, with a web of caustic light that brightens toward the hem), and plain slate trousers. System clothing layers for the SL upper/lower UV (layout learned with uvprobe.py, 2026-10-10: front torso u 0-0.5 centred at 0.25, back 0.5-1 centred at 0.75, neck at v=0, waist at v~0.625, arms below the waist line running shoulder->wrist left to right, hands from about u 0.7). python tidewater.py OUTDIR [--seed 3] [--sleeve 0.2] Claude Opus 5.5, 2026-10-10. """ import argparse import sys from pathlib import Path import numpy as np from PIL import Image sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "skin")) from mosaic import poisson, seam_field, wobble # noqa: E402 S = 1024 WAIST = 0.625 SHALLOWS = 0.48 # the gradient arrives here: the waistband of high-waisted jeans (2026-10-10) DEEP = np.array([22, 48, 80], np.float32) MID = np.array([26, 100, 122], np.float32) SHALLOW = np.array([72, 182, 176], np.float32) LIGHT = np.array([214, 248, 238], np.float32) SLATE = np.array([50, 55, 63], np.float32) def fabric(rng, amp=0.035): """Fine weave plus a little blotchy dye variation, as a multiplier around 1.""" ys, xs = np.mgrid[0:S, 0:S].astype(np.float32) weave = 0.5 * (np.sin(xs * np.pi / 1.5) * np.sin(ys * np.pi / 1.5)) grain = rng.normal(0, 1, (S, S)).astype(np.float32) grain = (grain + np.roll(grain, 1, 0) + np.roll(grain, 1, 1)) / 3 dye = wobble(S, rng, octaves=2) - 0.5 return 1 + amp * (0.4 * weave + 0.6 * grain) + 0.06 * dye def caustics(rng, tile=34): """Thin bright wavy network: Voronoi edges sampled through a smooth warp.""" pts = poisson(S, tile, rng) gap, _, _ = seam_field(S, pts) ys, xs = np.mgrid[0:S, 0:S] wx = ((wobble(S, rng) - 0.5) * 26).astype(int) wy = ((wobble(S, rng) - 0.5) * 26).astype(int) g = gap[np.clip(ys + wy, 0, S - 1), (xs + wx) % S] width = 0.8 + 1.4 * wobble(S, rng) ** 2 line = np.clip((width - g / 2) / 1.2, 0, 1) glow = np.clip((width * 3 - g / 2) / (width * 3), 0, 1) ** 3 * 0.35 shimmer = 0.15 + 0.85 * wobble(S, rng, octaves=4) ** 2 # some strands bright, most faint return np.clip((line + glow) * shimmer, 0, 1) def top(seed, sleeve): rng = np.random.default_rng(seed) v, u = (np.mgrid[0:S, 0:S].astype(np.float32) + 0.5) / S torso = v < WAIST t = np.where(torso, np.clip(v / SHALLOWS, 0, 1), 0.12) t = t ** 1.3 a = np.clip(t * 2, 0, 1)[..., None] b = np.clip(t * 2 - 1, 0, 1)[..., None] rgb = DEEP * (1 - a) + MID * a rgb = rgb * (1 - b) + SHALLOW * b rgb *= fabric(rng)[..., None] light = caustics(rng) * (0.02 + 0.75 * t ** 2.2) rgb = rgb * (1 - light[..., None]) + LIGHT * light[..., None] # coverage: scoop neck in front, short sleeves neck = np.where(u < 0.5, 0.014 + 0.07 * np.exp(-((u - 0.25) / 0.075) ** 2), 0.014) alpha = np.clip((v - neck) * S / 1.5, 0, 1) alpha = np.where(torso, alpha, np.clip((sleeve - u) * S / 1.5, 0, 1)) # bindings: a darker band at the neckline, sleeve ends and hem band = np.where(torso, (v - neck) * S < 7, (sleeve - u) * S < 8) | (torso & ((WAIST - v) * S < 6)) rgb = np.where(band[..., None], rgb * 0.55 + DEEP * 0.45, rgb) img = np.concatenate([rgb, alpha[..., None] * 255], axis=-1) return Image.fromarray(img.clip(0, 255).astype(np.uint8), "RGBA") def trousers(seed): rng = np.random.default_rng(seed + 100) rgb = SLATE * fabric(rng, 0.05)[..., None] img = np.concatenate([rgb, np.full((S, S, 1), 255, np.float32)], axis=-1) return Image.fromarray(img.clip(0, 255).astype(np.uint8), "RGBA") def main(): ap = argparse.ArgumentParser() ap.add_argument("outdir") ap.add_argument("--seed", type=int, default=3) ap.add_argument("--sleeve", type=float, default=0.2, help="sleeve end, as u along the arm (0 = shoulder)") a = ap.parse_args() out = Path(a.outdir) t = top(a.seed, a.sleeve) t.save(out / "tidewater-top.png") trousers(a.seed).save(out / "tidewater-trousers.png") prev = Image.new("RGBA", t.size, (224, 186, 160, 255)) Image.alpha_composite(prev, t).convert("RGB").resize((512, 512)).save(out / "tidewater-top-preview.jpg", quality=88) print("ok") if __name__ == "__main__": main()