"""Deterministic front-view guitar renderer and analytic arm.""" from __future__ import annotations import random from PIL import Image, ImageDraw CANVAS = 512 SCALE = 3 PALETTES = ( {"body": "#246a9a", "neck": "#b87832", "edge": "#182331", "bridge": "#f4d98c", "accent": "#e9f1f6"}, {"body": "#a74432", "neck": "#d09a4c", "edge": "#20242a", "bridge": "#dceef7", "accent": "#f6df9b"}, {"body": "#397a4b", "neck": "#bd743d", "edge": "#24222b", "bridge": "#f2d795", "accent": "#dcebf5"}, {"body": "#704c91", "neck": "#c18442", "edge": "#20232b", "bridge": "#dcecf5", "accent": "#f4dda1"}, ) def sample_scene(seed: int) -> dict: rng = random.Random(int(seed)) count = 4 + (int(seed) % 5) return { "string_count": count, "palette": rng.randrange(len(PALETTES)), "offset_x": rng.randint(-24, 24), "offset_y": rng.randint(-10, 12), "body_width": rng.randint(238, 270), "body_height": rng.randint(220, 246), "string_span": rng.randint(34, 48), "string_width": rng.choice([1.5, 1.8, 2.1, 2.4]), "marker_variant": rng.randrange(4), } def _p(value: float) -> int: return round(value * SCALE) def render(scene: dict) -> Image.Image: count = int(scene["string_count"]) colors = PALETTES[int(scene["palette"])] ox = int(scene["offset_x"]) oy = int(scene["offset_y"]) bw = int(scene["body_width"]) bh = int(scene["body_height"]) span = int(scene["string_span"]) string_w = float(scene["string_width"]) marker_variant = int(scene["marker_variant"]) image = Image.new("RGB", (CANVAS * SCALE, CANVAS * SCALE), "#f8f5ee") draw = ImageDraw.Draw(image) cx = 256 + ox top = 249 + oy bottom = top + bh neck_top = 78 + oy neck_half = 31 # Soft, offset grounding shadow. draw.ellipse((_p(cx - bw * .47 + 8), _p(bottom - 22), _p(cx + bw * .47 + 18), _p(bottom + 10)), fill="#ddd8ce") # Headstock, tuning pegs, and neck are drawn before the body so their joins # are visibly continuous but the instrument outline remains unambiguous. head = [ (_p(cx - 37), _p(neck_top - 42)), (_p(cx + 37), _p(neck_top - 42)), (_p(cx + 32), _p(neck_top + 8)), (_p(cx + 24), _p(neck_top + 25)), (_p(cx - 24), _p(neck_top + 25)), (_p(cx - 32), _p(neck_top + 8)), ] draw.polygon(head, fill=colors["neck"], outline=colors["edge"], width=_p(3)) for side in (-1, 1): for yy in (neck_top - 27, neck_top - 10, neck_top + 7): x1 = cx + side * 34 x2 = cx + side * 47 draw.line((_p(x1), _p(yy), _p(x2), _p(yy)), fill=colors["edge"], width=_p(3)) draw.ellipse((_p(x2 - 5), _p(yy - 5), _p(x2 + 5), _p(yy + 5)), fill=colors["accent"], outline=colors["edge"], width=_p(2)) draw.rounded_rectangle((_p(cx - neck_half), _p(neck_top), _p(cx + neck_half), _p(top + 104)), radius=_p(5), fill=colors["neck"], outline=colors["edge"], width=_p(4)) # A symmetric waisted guitar silhouette. pts = [ (cx, top), (cx - bw*.28, top + bh*.05), (cx - bw*.43, top + bh*.20), (cx - bw*.30, top + bh*.39), (cx - bw*.49, top + bh*.55), (cx - bw*.48, top + bh*.78), (cx - bw*.31, top + bh*.96), (cx, bottom), (cx + bw*.31, top + bh*.96), (cx + bw*.48, top + bh*.78), (cx + bw*.49, top + bh*.55), (cx + bw*.30, top + bh*.39), (cx + bw*.43, top + bh*.20), (cx + bw*.28, top + bh*.05), ] draw.polygon([(_p(x), _p(y)) for x, y in pts], fill=colors["body"], outline=colors["edge"], width=_p(5)) # Sound hole and rosette. Strings later cross these, as on a real guitar. sy = top + bh * .43 sr = 37 draw.ellipse((_p(cx-sr-5), _p(sy-sr-5), _p(cx+sr+5), _p(sy+sr+5)), fill=colors["accent"], outline=colors["edge"], width=_p(3)) draw.ellipse((_p(cx-sr), _p(sy-sr), _p(cx+sr), _p(sy+sr)), fill=colors["edge"]) # Small fret markers vary independently and never enter the bridge band. for index, yy in enumerate(range(neck_top + 62, top + 75, 31)): if (index + marker_variant) % 3 != 0: r = 3 if (index + marker_variant) % 2 else 4 draw.ellipse((_p(cx-r), _p(yy-r), _p(cx+r), _p(yy+r)), fill=colors["accent"]) draw.line((_p(cx-neck_half+3), _p(yy+12), _p(cx+neck_half-3), _p(yy+12)), fill="#6f5437", width=_p(1)) # The pale bridge is a visible counting window, not a hidden coordinate. bridge_y = top + bh * .73 bridge_w = max(105, span + 54) draw.rounded_rectangle((_p(cx-bridge_w/2), _p(bridge_y-14), _p(cx+bridge_w/2), _p(bridge_y+14)), radius=_p(5), fill=colors["bridge"]) # Equally separated continuous strings, spanning headstock through body. if count == 1: xs = [cx] else: xs = [cx - span/2 + index * span/(count-1) for index in range(count)] width = max(4, round(string_w * SCALE)) for x in xs: draw.line((_p(x), _p(neck_top-31), _p(x), _p(top+bh*.88)), fill=colors["edge"], width=width) return image.resize((CANVAS, CANVAS), Image.Resampling.LANCZOS) def analytic_gold(scene: dict) -> str: return str(int(scene["string_count"])) def margin(scene: dict) -> float: count = int(scene["string_count"]) spacing = float(scene["string_span"]) / (count - 1) return spacing - float(scene["string_width"]) def is_quarantined(scene: dict) -> bool: return margin(scene) < 3.0 def latent_symmetries(scene: dict) -> list[tuple[str, dict]]: return []