#!/usr/bin/env python3 """ make_voyager_bandeau.py -- golden plaited-weave band with black rope trim and fringe for the female voyager bandeau top (lena_voyager_bandeau_v1). python tools/tailor/textures/make_voyager_bandeau.py Derived from the reference renders `fc-voyager-kilt-gpt-piece-top{,-front,-back}.png` (docs repo): a diagonal basket plait in warm golden tan, a black braided-rope band near each edge, and a short strand fringe extending to both the top and bottom edges. POSITIONED TILE, same doctrine as make_kilt_cloth.py: spans the bandeau panel's full 170 mm height exactly once (dpi = SIZE / (170 / 25.4)), so every zone lands at a fixed garment height: garment y (mm, hem=0) zone 0-15 bottom fringe strands (painted, not geometry -- v1) 15-27 black rope band (braided chevrons) 27-143 diagonal basket plait 143-155 black rope band 155-170 top fringe strands (tips ragged toward y=170) Horizontally the 170 mm tile repeats ~3.2x across the 550 mm panel; the plait pitch (17 mm) and rope chevron pitch (32 px) both divide the tile exactly, so the wrap is seamless. IF THE ROPE BANDS RENDER SWAPPED TOP-FOR-BOTTOM, MD anchored the texture the other way up -- regenerate with FLIP=True rather than touching the pattern. (Anchor per probe 2026-08-18: pattern y=0 (hem) maps to image v=0.5 and wraps.) Albedo baked in (pipeline has no alpha, texture replaces base color), warm ramp r>g>b, deterministic LCG noise. """ import os from PIL import Image HERE = os.path.dirname(os.path.abspath(__file__)) OUT = os.path.join(HERE, "voyager_bandeau.png") SIZE = 1024 # px, square CLOTH_MM = 170.0 # spans the bandeau panel height exactly once DPI = SIZE / (CLOTH_MM / 25.4) FLIP = False # set True if MD anchors the texture upside-down PX_PER_MM = SIZE / CLOTH_MM # zone boundaries in garment mm (hem = 0) BOT_FRINGE_TOP = 15.0 BOT_ROPE_TOP = 27.0 TOP_ROPE_BOT = 143.0 TOP_FRINGE_BOT = 155.0 PLAIT_PITCH_MM = 17.0 # strand pitch; 170/17 = 10 cells -> seamless wrap ROPE_CHEVRON_PX = 32 # divides 1024 -> seamless wrap BASE = (203, 165, 110) # golden tan plait (reference lit mean ~(200,165,115)) ROPE = (46, 40, 32) # near-black rope, warm ROPE_HI = (92, 80, 62) # braid ridge highlight FRINGE = (191, 152, 100) FRINGE_GAP = (140, 108, 70) SHADOW = (74, 60, 44) # past a fringe tip: painted shadow (no alpha in lane) OVER_LIFT = 14 UNDER_DROP = 18 ROUND_SHADE = 12 SLUB_RANGE = 8 MOTTLE = 10 MOTTLE_CELL = 64 def lcg(seed): state = seed while True: state = (1103515245 * state + 12345) % (2 ** 31) yield state / float(2 ** 31) def value_noise(cells, seed): g = lcg(seed) grid = [[(next(g) * 2.0 - 1.0) for _ in range(cells + 1)] for _ in range(cells + 1)] def sample(x, y): fx, fy = x * cells / SIZE, y * cells / SIZE x0, y0 = int(fx), int(fy) tx, ty = fx - x0, fy - y0 tx, ty = tx * tx * (3 - 2 * tx), ty * ty * (3 - 2 * ty) a = grid[y0][x0] * (1 - tx) + grid[y0][x0 + 1] * tx b = grid[y0 + 1][x0] * (1 - tx) + grid[y0 + 1][x0 + 1] * tx return a * (1 - ty) + b * ty return sample def mm_from_row(y): """Garment height in mm for an image row (anchor: hem -> v=0.5, wraps).""" v = (y + 0.5) / SIZE return CLOTH_MM - CLOTH_MM * ((v - 0.5) % 1.0) def plait(xm, ym, slub_a, slub_b): """Diagonal basket plait: two +/-45-degree strand families, over-under. Returns a brightness delta to apply on BASE.""" u = (xm + ym) / PLAIT_PITCH_MM w = (xm - ym) / PLAIT_PITCH_MM a, b = int(u // 1), int(w // 1) fu, fw = u - a, w - b over_a = ((a + b) % 2) == 0 if over_a: # family A on top: rounded across its width (fw), slub along it c = abs(fw - 0.5) * 2.0 d = OVER_LIFT - int(ROUND_SHADE * c * c) d += slub_a[a % len(slub_a)] if fw < 0.06 or fw > 0.94: # crevice against the under strand d -= 26 else: c = abs(fu - 0.5) * 2.0 d = OVER_LIFT - int(ROUND_SHADE * c * c) - UNDER_DROP // 2 d += slub_b[b % len(slub_b)] if fu < 0.06 or fu > 0.94: d -= 26 return d def main(): g = lcg(20260818) slub_a = [int((next(g) * 2.0 - 1.0) * SLUB_RANGE) for _ in range(64)] slub_b = [int((next(g) * 2.0 - 1.0) * SLUB_RANGE) for _ in range(64)] tip_noise = [next(g) for _ in range(64)] mottle = value_noise(SIZE // MOTTLE_CELL, seed=777) img = Image.new("RGB", (SIZE, SIZE)) px = img.load() strand_w = 16 # fringe strand width px (~2.7 mm) for y in range(SIZE): mm = mm_from_row(y) ym = mm for x in range(SIZE): xm = x / PX_PER_MM wear = int(mottle(x, y) * MOTTLE) if mm < BOT_FRINGE_TOP or mm > TOP_FRINGE_BOT: # fringe: vertical strands, ragged tips toward the edge sid = (x // strand_w) % len(tip_noise) ragged = tip_noise[sid] * 8.0 past_tip = (mm < ragged if mm < BOT_FRINGE_TOP else mm > CLOTH_MM - ragged) if past_tip: base, d = SHADOW, wear // 2 else: u = x % strand_w base = FRINGE_GAP if u < 2 else FRINGE d = slub_a[sid] + wear elif mm < BOT_ROPE_TOP or mm > TOP_ROPE_BOT: # braided rope: diagonal chevron ridges phase = ((x + int(mm * PX_PER_MM) * 2) % ROPE_CHEVRON_PX) \ / float(ROPE_CHEVRON_PX) base = ROPE_HI if phase < 0.30 else ROPE d = wear // 2 else: base = BASE d = plait(xm, ym, slub_a, slub_b) + wear r = base[0] + d gg = base[1] + int(d * 0.82) b = base[2] + int(d * 0.66) px[x, y] = (max(0, min(255, r)), max(0, min(255, gg)), max(0, min(255, b))) if FLIP: img = img.transpose(Image.FLIP_TOP_BOTTOM) img.save(OUT, dpi=(DPI, DPI)) print("wrote %s (%dx%d, dpi %.2f -> %.0f mm of cloth; ropes %d-%d & %d-%d mm, " "fringe 0-%d & %d-%d mm)" % (OUT, SIZE, SIZE, DPI, CLOTH_MM, BOT_FRINGE_TOP, BOT_ROPE_TOP, TOP_ROPE_BOT, TOP_FRINGE_BOT, BOT_FRINGE_TOP, TOP_FRINGE_BOT, CLOTH_MM)) if __name__ == "__main__": main()