#!/usr/bin/env python3 """ Generate the Institution suite's Workshop Preview.png images. Pure stdlib -- no Pillow, no CUDA, no AI/diffusion, nothing ripped. Every pixel is drawn by the code below, exactly like the per-repo Tools/make_textures.py sprite generators, so the copyright story stays clean. Tools/make_preview.py # run from anywhere; writes into every sibling repo Reuses the same from-scratch machinery the texture generators use: a hand-rolled RGBA PNG writer (zlib + struct, see png()) and analytic signed-distance-field shapes supersampled for clean edges (see _raster). Nothing beyond the standard library, so it runs on a box with no image tools at all. Writes About/Preview.png (640x360, a safe 16:9 Workshop size) into each of: rimworld-institution flagship: a barred cell window over the nature->nurture crime spectrum rimworld-core the invisible engine: a nature x nurture node feeding a 0..1 spectrum bar rimworld-contraband a filed shiv against a brick wall breached by an escape tunnel rimworld-justice a level balance -- the corrections layer weighing what a pawn has done rimworld-gangs a network of pawn tokens, two rival clusters bridged by a contraband line They are one SET: shared muted institutional green-grey / tin palette, the same embossed-tin border, the same faint grain, and a shared propensity spectrum ribbon (green -> amber -> red) along the bottom of every image -- the suite's signature, the spectrum every layer reads from. """ import os import zlib import struct import math REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # rimworld-institution SUITE = os.path.dirname(REPO) # holds all the sibling repos W, H = 640, 360 AS = W / H # aspect; everything is normalised by HEIGHT so circles stay round SS = int(os.environ.get("SS", "3")) # --------------------------------------------------------------------------- # PNG encoder + supersampled rasteriser (pure stdlib) -- same as make_textures.py # --------------------------------------------------------------------------- def png(path, w, h, pixel_fn): """Write an 8-bit RGBA PNG from scratch. pixel_fn(x, y) -> (r, g, b, a).""" raw = bytearray() for y in range(h): raw.append(0) # filter byte: 0 = None, once per scanline row = bytearray() for x in range(w): r, g, b, a = pixel_fn(x, y) row += bytes((r & 255, g & 255, b & 255, a & 255)) raw += row def chunk(typ, data): return (struct.pack(">I", len(data)) + typ + data + struct.pack(">I", zlib.crc32(typ + data) & 0xffffffff)) ihdr = struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0) # 8-bit depth, colour type 6 = RGBA idat = zlib.compress(bytes(raw), 9) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "wb") as f: f.write(b"\x89PNG\r\n\x1a\n") f.write(chunk(b"IHDR", ihdr)) f.write(chunk(b"IDAT", idat)) f.write(chunk(b"IEND", b"")) def _raster(w, h, sample, ss=SS): """Wrap an analytic sample(fx, fy) -> (r, g, b, a) into a supersampled pixel_fn.""" inv = 1.0 / ss n = ss * ss def pixel(x, y): pr = pg = pb = pa = 0.0 for j in range(ss): fy = y + (j + 0.5) * inv for i in range(ss): r, g, b, a = sample(x + (i + 0.5) * inv, fy) if a: af = a * (1.0 / 255.0) pr += r * af pg += g * af pb += b * af pa += af if pa <= 1e-6: return (0, 0, 0, 0) return (_cb(pr / pa), _cb(pg / pa), _cb(pb / pa), _cb(pa / n * 255.0)) return pixel # --------------------------------------------------------------------------- # colour + geometry helpers (normalised coords; u in 0..AS, v in 0..1) # --------------------------------------------------------------------------- def _cb(x): x = int(x) return 0 if x < 0 else (255 if x > 255 else x) def _clamp01(x): return 0.0 if x < 0.0 else (1.0 if x > 1.0 else x) def _shade(c, f): return (_cb(c[0] * f), _cb(c[1] * f), _cb(c[2] * f)) def _mix(a, b, t): return (_cb(a[0] + (b[0] - a[0]) * t), _cb(a[1] + (b[1] - a[1]) * t), _cb(a[2] + (b[2] - a[2]) * t)) def _grain(ix, iy): """Deterministic faint speckle so fills aren't dead-flat.""" n = (ix * 374761393 + iy * 668265263) & 0xffffffff n ^= (n >> 13) n = (n * 1274126177) & 0xffffffff return ((n >> 9) & 7) - 3 # -3..+4 def _grainy(c, u, v): g = _grain(int(u * 300), int(v * 300)) return (_cb(c[0] + g), _cb(c[1] + g), _cb(c[2] + g)) def _seg(px, py, ax, ay, bx, by): """Distance from P to segment AB, and the clamped projection param t in [0, 1].""" dx, dy = bx - ax, by - ay l2 = dx * dx + dy * dy if l2 < 1e-12: return math.hypot(px - ax, py - ay), 0.0 t = ((px - ax) * dx + (py - ay) * dy) / l2 t = 0.0 if t < 0 else (1.0 if t > 1 else t) cx, cy = ax + dx * t, ay + dy * t return math.hypot(px - cx, py - cy), t def _ellipse(u, v, cx, cy, rx, ry): """Approximate signed distance to an ellipse. Negative inside.""" k = math.hypot((u - cx) / rx, (v - cy) / ry) return (k - 1.0) * min(rx, ry) def _rrect(u, v, cx, cy, hw, hh, r): """Signed distance to a rounded rectangle. Negative inside.""" qx = abs(u - cx) - (hw - r) qy = abs(v - cy) - (hh - r) ox = qx if qx > 0 else 0.0 oy = qy if qy > 0 else 0.0 return math.hypot(ox, oy) + min(max(qx, qy), 0.0) - r def _convex(u, v, pts): """Signed distance to a convex polygon (negative inside). Winding-agnostic.""" n = len(pts) cxp = sum(p[0] for p in pts) / n cyp = sum(p[1] for p in pts) / n dmax = -1e9 for i in range(n): ax, ay = pts[i] bx, by = pts[(i + 1) % n] ex, ey = bx - ax, by - ay L = math.hypot(ex, ey) or 1e-9 nx, ny = ey / L, -ex / L # a normal to the edge dp = (u - ax) * nx + (v - ay) * ny dc = (cxp - ax) * nx + (cyp - ay) * ny # which side the interior is on if dc > 0: dp = -dp if dp > dmax: dmax = dp return dmax # --------------------------------------------------------------------------- # shared palette -- muted institutional green-grey / tin, dark outline # --------------------------------------------------------------------------- OUTLINE = (22, 26, 20) BG_TOP = (56, 64, 55) BG_BOT = (31, 37, 31) TIN = (138, 142, 126) TIN_HI = (178, 182, 162) TIN_DK = (92, 96, 82) TIN_DDK = (62, 66, 56) STEEL = (180, 184, 178) STEEL_HI = (210, 214, 208) STEEL_DK = (120, 124, 118) BRICK = (94, 98, 88) BRICK2 = (110, 114, 102) MORTAR = (54, 58, 50) EARTH = (46, 39, 29) EARTH_DK = (24, 20, 14) # the propensity spectrum: calm green (low) -> amber -> dangerous red (high) SPEC_LOW = (104, 150, 100) SPEC_MID = (198, 160, 74) SPEC_HI = (176, 68, 54) def spec(t): """Colour along the nature->nurture propensity spectrum, t in 0..1.""" t = _clamp01(t) if t < 0.5: return _mix(SPEC_LOW, SPEC_MID, t * 2.0) return _mix(SPEC_MID, SPEC_HI, (t - 0.5) * 2.0) # --------------------------------------------------------------------------- # shared frame + background + spectrum ribbon (identical on every preview) # --------------------------------------------------------------------------- CX, CY = AS / 2.0, 0.5 FR_M = 0.050 # outer margin FR_R = 0.038 # corner radius FR_W = 0.026 # frame band width FHW, FHH = CX - FR_M, CY - FR_M IL, IR = FR_M + FR_W + 0.004, AS - FR_M - FR_W - 0.004 # inner content bounds IT, IB = FR_M + FR_W + 0.004, 1.0 - FR_M - FR_W - 0.004 RIB_B = IB - 0.006 RIB_T = RIB_B - 0.036 RIB_L, RIB_R = IL + 0.004, IR - 0.004 def _bg(u, v): c = _mix(BG_TOP, BG_BOT, _clamp01(v)) dx, dy = (u - CX) / CX, (v - CY) / CY c = _shade(c, 1.0 - 0.24 * _clamp01(dx * dx + dy * dy)) return _grainy(c, u, v) def _frame(u, v, d): """Embossed-tin border colour for a point whose rounded-rect distance is d.""" if d > -0.004: return OUTLINE # crisp outer edge line p = (-0.004 - d) / (FR_W - 0.004) # 0 outer .. 1 inner if p > 0.90: return OUTLINE # crisp inner edge line nx, ny = (u - CX), (v - CY) lit = -(nx * 0.55 + ny * 0.85) # light from the top-left if lit > 0: c = _mix(TIN, TIN_HI, _clamp01(lit * 2.4)) else: c = _mix(TIN, TIN_DDK, _clamp01(-lit * 2.4)) return _grainy(c, u, v) def _ribbon(u, v): t = (u - RIB_L) / (RIB_R - RIB_L) c = _shade(spec(t), 0.86) if v - RIB_T < 0.004: return OUTLINE # dark lintel over the ribbon if RIB_B - v < 0.004: c = _shade(c, 0.62) if (t * 8.0) % 1.0 < 0.035: # eighth-tick divisions c = _shade(c, 0.68) return _grainy(c, u, v) def preview(motif): """Compose background + shared ribbon + motif + border into one supersampled pixel_fn.""" def sample(fx, fy): u, v = fx / H, fy / H d = _rrect(u, v, CX, CY, FHW, FHH, FR_R) if d > 0.004: return _grainy(_shade(BG_BOT, 0.72), u, v) + (255,) # rounded corner falloff if d > -FR_W: return _frame(u, v, d) + (255,) if RIB_T <= v <= RIB_B and RIB_L <= u <= RIB_R: return _ribbon(u, v) + (255,) m = motif(u, v) if m is not None: return m if len(m) == 4 else (m + (255,)) return _bg(u, v) + (255,) return _raster(W, H, sample) # =========================================================================== # INSTITUTION -- the flagship. A barred cell window; behind the bars the whole # nature->nurture crime spectrum glows. Prison + a spectrum of crime, in one image. # =========================================================================== WIN_CX, WIN_CY = CX, 0.45 WIN_HW, WIN_HH = 0.62, 0.30 NBARS = 6 def m_institution(u, v): d = _rrect(u, v, WIN_CX, WIN_CY, WIN_HW, WIN_HH, 0.028) if d > 0.030: return None if d > -0.006: return OUTLINE # window outer edge if d > -0.036: # tin window frame, bevelled nx, ny = (u - WIN_CX), (v - WIN_CY) lit = -(nx * 0.5 + ny * 0.9) c = _mix(TIN, TIN_HI, _clamp01(lit * 3.0)) if lit > 0 else _mix(TIN, TIN_DK, _clamp01(-lit * 3.0)) return _grainy(c, u, v) L, R = WIN_CX - WIN_HW, WIN_CX + WIN_HW t = (u - L) / (R - L) # bars: NBARS vertical + two horizontal, in front of the spectrum s = (2 * WIN_HW) / NBARS i = int((u - L) / s) xc = L + s * (i + 0.5) dvb = abs(u - xc) bw = 0.016 hb = min(abs(v - (WIN_CY - 0.175)), abs(v - (WIN_CY + 0.175))) hh = 0.014 is_v = dvb < bw is_h = hb < hh if is_v or is_h: vertical = is_v and (not is_h or (dvb / bw) <= (hb / hh)) if vertical: if dvb > bw - 0.0035: return OUTLINE p = (u - (xc - bw)) / (2 * bw) # 0 left .. 1 right c = _mix(STEEL_DK, STEEL, 1.0 - p * 0.85) c = _mix(c, STEEL_HI, _clamp01(0.5 - p) * 0.7) return _grainy(c, u, v) else: if hb > hh - 0.0035: return OUTLINE near = WIN_CY - 0.175 if abs(v - (WIN_CY - 0.175)) < abs(v - (WIN_CY + 0.175)) else WIN_CY + 0.175 p = (v - (near - hh)) / (2 * hh) c = _mix(STEEL_DK, STEEL, 1.0 - p * 0.85) c = _mix(c, STEEL_HI, _clamp01(0.5 - p) * 0.7) return _grainy(c, u, v) # interior: the spectrum, brighter through the middle so it reads as light behind bars base = spec(t) gy = 1.0 - _clamp01(abs(v - WIN_CY) / WIN_HH) base = _shade(base, 0.58 + 0.55 * gy) if d > -0.020: # inner shadow just inside the frame base = _shade(base, 0.80) return _grainy(base, u, v) # =========================================================================== # CORE -- the invisible foundation. Two faint axes, NATURE (vertical) x NURTURE # (horizontal), meet at the engine node, which drives the 0..1 spectrum bar below. # Deliberately subdued: on its own it changes nothing you can see. # =========================================================================== NODE_X, NODE_Y = CX, 0.38 BAR_Y, BAR_HW, BAR_HH = 0.60, 0.62, 0.052 def m_core(u, v): # 0..1 spectrum bar (the hero, low and central) db = _rrect(u, v, CX, BAR_Y, BAR_HW, BAR_HH, 0.024) if db <= 0.005: if db > -0.005: return OUTLINE t = (u - (CX - BAR_HW)) / (2 * BAR_HW) c = spec(t) if (t * 10.0) % 1.0 < 0.028 and abs(v - BAR_Y) > BAR_HH - 0.016: c = _shade(c, 0.6) # scale ticks along the rim if abs(v - (BAR_Y - BAR_HH * 0.45)) < 0.006: c = _mix(c, (255, 255, 255), 0.12) # a faint sheen line return _grainy(c, u, v) # engine node at the crossing dn = math.hypot(u - NODE_X, v - NODE_Y) if dn < 0.058: if dn > 0.050: return OUTLINE c = _mix(TIN, STEEL_HI, _clamp01(1.0 - dn / 0.050)) return _grainy(c, u, v) # faint NATURE x NURTURE axes behind everything if abs(u - NODE_X) < 0.0035 and 0.14 < v < 0.62: return _grainy(TIN_DK, u, v) if abs(v - NODE_Y) < 0.0035 and (CX - 0.54) < u < (CX + 0.54): return _grainy(TIN_DK, u, v) # end caps on the axes -> small pips (nature high/low, nurture low/high) for (px, py) in ((NODE_X, 0.15), (NODE_X, 0.61), (CX - 0.54, NODE_Y), (CX + 0.54, NODE_Y)): if math.hypot(u - px, v - py) < 0.016: return _grainy(TIN, u, v) return None # =========================================================================== # CONTRABAND -- a filed shiv lying against a brick wall that an escape tunnel has # breached. The physical smuggling loop: a whittled weapon and a hole under the wall. # =========================================================================== BX, BY, BRX, BRY = CX + 0.44, 0.54, 0.28, 0.25 # the tunnel breach def _brick(u, v): bh, bw = 0.078, 0.180 row = int((v - IT) / bh) x = u + (bw * 0.5 if row % 2 else 0.0) col = int(x / bw) ly = (v - IT) - row * bh lx = x - col * bw if ly < 0.012 or lx < 0.012: return MORTAR h = (row * 928371 + col * 123457) & 255 return _shade(_mix(BRICK, BRICK2, h / 255.0), 0.94 + 0.12 * (((h >> 3) & 3) / 3.0)) def _breach(u, v): ang = math.atan2(v - BY, u - BX) wob = 0.028 * math.sin(ang * 7.0 + 0.7) + 0.020 * math.sin(ang * 3.0) k = math.hypot((u - BX) / (BRX + wob), (v - BY) / (BRY + wob)) return (k - 1.0) * min(BRX, BRY) def _shiv(u, v): Bx, By = 0.30, 0.80 # cloth grip low-left Jx, Jy = 0.44, 0.60 Tx, Ty = 0.78, 0.24 # filed point high-right db, tb = _seg(u, v, Jx, Jy, Tx, Ty) half = 0.030 * (1.0 - tb) + 0.004 * tb d_blade = db - half dg, tg = _seg(u, v, Bx, By, Jx, Jy) d_grip = dg - 0.040 d = min(d_blade, d_grip) if d > 0: return None if d > -0.006: return OUTLINE if d_grip <= d_blade: wind = (110, 110, 104) if (tg * 4.3) % 1.0 < 0.34 else (150, 150, 142) return _grainy(_shade(wind, 0.95 + 0.10 * tg), u, v) bevel = 1.0 - _clamp01(db / max(1e-6, half)) c = _mix(STEEL_DK, STEEL, _clamp01(bevel * 1.6)) c = _mix(c, STEEL_HI, _clamp01(bevel - 0.55) * 1.4) return _grainy(c, u, v) def m_contraband(u, v): s = _shiv(u, v) # the shiv sits on top of the wall if s is not None: return s db = _breach(u, v) if db < 0: # tunnel mouth: dark earth, deeper = darker f = _clamp01(-db / BRY) return _grainy(_mix(EARTH, EARTH_DK, f), u, v) if db < 0.020: # broken, crumbling brick edge return _grainy(_shade(MORTAR, 0.7), u, v) return _grainy(_brick(u, v), u, v) # =========================================================================== # JUSTICE -- a level balance. Corrections weighing what a pawn has actually done: # classification, deterrence, discipline. A gavel-less balance, held even. # =========================================================================== J_BEAM_Y = 0.28 J_BEAM_HW = 0.40 J_BASE_Y = 0.74 J_PAN_Y = 0.54 def _pan(u, v, px): d = _ellipse(u, v, px, J_PAN_Y, 0.15, 0.058) if v < J_PAN_Y: # keep the lower half -> a shallow bowl d = max(d, (J_PAN_Y - v)) return d def m_justice(u, v): OW = 0.006 d_post = _rrect(u, v, CX, (J_BASE_Y + J_BEAM_Y) / 2.0, 0.018, (J_BASE_Y - J_BEAM_Y) / 2.0, 0.006) d_base = min(_rrect(u, v, CX, J_BASE_Y, 0.150, 0.026, 0.012), _convex(u, v, [(CX - 0.055, J_BASE_Y - 0.055), (CX + 0.055, J_BASE_Y - 0.055), (CX + 0.100, J_BASE_Y - 0.020), (CX - 0.100, J_BASE_Y - 0.020)])) d_beam = _rrect(u, v, CX, J_BEAM_Y, J_BEAM_HW, 0.015, 0.008) d_fulc = _convex(u, v, [(CX, J_BEAM_Y - 0.055), (CX + 0.052, J_BEAM_Y), (CX - 0.052, J_BEAM_Y)]) d_panL = _pan(u, v, CX - J_BEAM_HW) d_panR = _pan(u, v, CX + J_BEAM_HW) d_solid = min(d_post, d_base, d_beam, d_fulc, d_panL, d_panR) # hangers: a thin line from each beam end down to its pan dcl, _ = _seg(u, v, CX - J_BEAM_HW, J_BEAM_Y, CX - J_BEAM_HW, J_PAN_Y - 0.05) dcr, _ = _seg(u, v, CX + J_BEAM_HW, J_BEAM_Y, CX + J_BEAM_HW, J_PAN_Y - 0.05) d_chain = min(dcl, dcr) - 0.004 if d_chain <= 0 and d_chain < d_solid: return _grainy(TIN_DK, u, v) if d_chain > -0.004 else _grainy(TIN, u, v) if d_solid > 0: return None if d_solid > -OW: return OUTLINE # tin, lit from the top lit = 1.0 - _clamp01((v - J_BEAM_Y) / (J_BASE_Y - J_BEAM_Y)) c = _mix(TIN_DK, TIN, 0.55 + 0.45 * lit) if d_solid <= d_panL or d_solid <= d_panR or d_solid <= d_beam: c = _mix(c, TIN_HI, 0.25 * lit) return _grainy(c, u, v) # =========================================================================== # GANGS -- a contraband network. Pawn tokens band into two rival clusters (amber # vs red) that a bright contraband line bridges through a broker in the middle. # =========================================================================== GANG_A, GANG_A_HI = (190, 150, 72), (216, 180, 98) GANG_B, GANG_B_HI = (176, 70, 56), (208, 100, 84) CONDUIT = (200, 202, 182) NODES = [ (CX - 0.50, 0.34, 'A'), (CX - 0.62, 0.50, 'A'), (CX - 0.52, 0.66, 'A'), (CX - 0.36, 0.48, 'A'), (CX, 0.44, 'X'), (CX + 0.36, 0.36, 'B'), (CX + 0.54, 0.50, 'B'), (CX + 0.42, 0.66, 'B'), (CX + 0.24, 0.52, 'B'), ] EDGES = [ (0, 1, 'A'), (1, 2, 'A'), (0, 3, 'A'), (3, 2, 'A'), (1, 3, 'A'), (5, 6, 'B'), (6, 7, 'B'), (5, 8, 'B'), (8, 7, 'B'), (6, 8, 'B'), (3, 4, 'X'), (4, 8, 'X'), ] def _token(u, v, nx, ny, base, hi): d_head = math.hypot(u - nx, v - (ny - 0.024)) - 0.020 d_body = _rrect(u, v, nx, ny + 0.014, 0.032, 0.024, 0.012) d = min(d_head, d_body) if d > 0: return None if d > -0.005: return OUTLINE return _grainy(hi if d_head < d_body else base, u, v) def m_gangs(u, v): for (nx, ny, g) in NODES: # tokens sit on top of the wires if abs(u - nx) < 0.06 and abs(v - ny) < 0.06: if g == 'A': r = _token(u, v, nx, ny, GANG_A, GANG_A_HI) elif g == 'B': r = _token(u, v, nx, ny, GANG_B, GANG_B_HI) else: r = _token(u, v, nx, ny, TIN, TIN_HI) if r is not None: return r best, kind, bt = 1e9, None, 0.0 for (i, j, k) in EDGES: ax, ay = NODES[i][0], NODES[i][1] bx, by = NODES[j][0], NODES[j][1] dd, tt = _seg(u, v, ax, ay, bx, by) if dd < best: best, kind, bt = dd, k, tt hw = 0.005 if best < hw: if kind == 'X': # the contraband conduit: a bright dashed line return _grainy(CONDUIT, u, v) if int(bt * 24) % 2 == 0 else None col = _shade(GANG_A, 0.62) if kind == 'A' else _shade(GANG_B, 0.62) if best > hw - 0.0018: return _shade(col, 0.7) return _grainy(col, u, v) return None # =========================================================================== # verify: decode a PNG we wrote and confirm it is non-trivial (varied pixels) # =========================================================================== def verify(path): data = open(path, "rb").read() assert data[:8] == b"\x89PNG\r\n\x1a\n", "not a PNG" i, idat, w, h = 8, b"", 0, 0 while i < len(data): ln = struct.unpack(">I", data[i:i + 4])[0] typ = data[i + 4:i + 8] body = data[i + 8:i + 8 + ln] if typ == b"IHDR": w, h = struct.unpack(">II", body[:8]) elif typ == b"IDAT": idat += body i += 12 + ln raw = zlib.decompress(idat) # rows: filter byte 0 + w*4 RGBA bytes stride = w * 4 + 1 seen = set() lo = [255, 255, 255] hi = [0, 0, 0] for y in range(0, h, 7): # sparse sample is plenty to prove variety base = y * stride + 1 for x in range(0, w, 7): o = base + x * 4 px = (raw[o], raw[o + 1], raw[o + 2]) seen.add(px) for c in range(3): lo[c] = min(lo[c], px[c]) hi[c] = max(hi[c], px[c]) spread = max(hi[c] - lo[c] for c in range(3)) return {"bytes": len(data), "w": w, "h": h, "distinct": len(seen), "spread": spread} TARGETS = [ ("institution", m_institution, "barred cell window over the nature->nurture crime spectrum"), ("core", m_core, "nature x nurture engine node driving a 0..1 spectrum bar"), ("contraband", m_contraband, "filed shiv against a brick wall breached by an escape tunnel"), ("justice", m_justice, "a level balance weighing what a pawn has done"), ("gangs", m_gangs, "two rival pawn clusters bridged by a contraband line"), ] def main(): for name, motif, desc in TARGETS: path = os.path.join(SUITE, "rimworld-" + name, "About", "Preview.png") png(path, W, H, preview(motif)) info = verify(path) ok = "OK" if info["distinct"] > 200 and info["spread"] > 80 else "?? CHECK" print(f"{ok} {os.path.relpath(path, SUITE)} " f"{info['w']}x{info['h']} {info['bytes']:,}B " f"distinct~{info['distinct']} spread={info['spread']} -- {desc}") if __name__ == "__main__": main()