""" generate.py — Deterministic skeleton-GIF generator. Pipeline: prompt -> (keyword / zero-shot LLM) -> (action, emotion) -> procedural skeleton keyframes -> PIL frames (black bg, white bones) -> GIF Hard guarantees: * Output is ALWAYS a .gif * Output is ALWAYS a skeleton (drawn by us, no diffusion) * Emotion is visible on the skeleton face AND body posture * Zero hallucination (classification is closed-set; rendering is deterministic) Usage: python generate.py "a sad man reading a book" [--out ./outputs] [--debug] """ from __future__ import annotations import argparse import logging import math import os import re import sys import time import traceback from datetime import datetime from pathlib import Path from typing import Callable, Dict, Tuple from PIL import Image, ImageDraw # --------------------------------------------------------------------------- # Logging # --------------------------------------------------------------------------- logger = logging.getLogger("generate") def _setup_logging(debug: bool) -> None: level = logging.DEBUG if debug else logging.INFO logging.basicConfig( level=level, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%H:%M:%S", ) # Silence noisy third-party loggers so our debug output stays readable. for noisy in ("httpcore", "httpx", "urllib3", "transformers", "filelock", "huggingface_hub", "PIL"): logging.getLogger(noisy).setLevel(logging.WARNING) logger.setLevel(level) logger.debug("Logging initialised at level=%s", logging.getLevelName(level)) # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- CANVAS = 512 N_FRAMES = 24 FPS = 10 FRAME_MS = int(1000 / FPS) BG_COLOR = (8, 8, 14) # near-black BONE_COLOR = (240, 240, 240) # bone-white JOINT_COLOR = (255, 255, 255) ACCENT_COLOR = (200, 60, 60) # for "book" / props BONE_WIDTH = 6 JOINT_RADIUS = 5 FLOOR_Y = 462 # where skeleton feet meet the ground # Joint IDs HEAD, NECK, TORSO, PELVIS = 0, 1, 2, 3 LSH, LEL, LHA = 4, 5, 6 RSH, REL, RHA = 7, 8, 9 LHP, LKN, LFT = 10, 11, 12 RHP, RKN, RFT = 13, 14, 15 SKELETON_EDGES: Tuple[Tuple[int, int], ...] = ( (NECK, TORSO), (TORSO, PELVIS), (NECK, LSH), (LSH, LEL), (LEL, LHA), (NECK, RSH), (RSH, REL), (REL, RHA), (PELVIS, LHP), (LHP, LKN), (LKN, LFT), (PELVIS, RHP), (RHP, RKN), (RKN, RFT), ) # Rest pose (standing, arms down). Coords in image space (y grows downward). REST_POSE: Dict[int, Tuple[float, float]] = { HEAD: (256.0, 110.0), NECK: (256.0, 155.0), TORSO: (256.0, 215.0), PELVIS: (256.0, 290.0), LSH: (220.0, 160.0), LEL: (205.0, 220.0), LHA: (198.0, 280.0), RSH: (292.0, 160.0), REL: (307.0, 220.0), RHA: (314.0, 280.0), LHP: (232.0, 300.0), LKN: (228.0, 370.0), LFT: (225.0, 440.0), RHP: (280.0, 300.0), RKN: (284.0, 370.0), RFT: (287.0, 440.0), } ACTION_LABELS = [ "dancing", "walking", "running", "reading", "waving", "sitting", "jumping", "standing_idle", "fighting", "thinking", "working", "eating", "drinking", "sleeping", "crying", "laughing", "praying", "clapping", "writing", "stretching", "kicking", "vacuuming", "sweeping", "cooking", "washing", "gardening", "cleaning", "dusting", "mopping", "ironing", "polishing", # Sports + gaming "football", "cricket", "basketball", "tennis", "baseball", "golf", "bowling", "skateboarding", "gaming", # Body / motion "bowing", "hugging", "kissing", "handshake", "pointing", "climbing", "falling", "crawling", "rolling", "juggling", # Water / snow / board sports "swimming", "diving", "surfing", "skating", "skiing", "biking", # Vehicle / locomotion "driving", "riding_horse", "rowing", # Involuntary / body care "sneezing", "coughing", "yawning", "shivering", "scratching", "shaving", "brushing_teeth", "combing_hair", "dressing", # Load / force "carrying", "lifting", "pushing", "pulling", "throwing", "catching", # Precision / craft "fishing", "shooting", "archery", "painting", "drawing", "sculpting", "photographing", # Sound / voice / teaching "singing", "whistling", "yelling", "teaching", "presenting", # Instruments "playing_guitar", "playing_piano", "playing_drums", # Modern life "shopping", "texting", "meditating", ] EMOTION_LABELS = [ "happy", "sad", "angry", "tired", "excited", "neutral", "scared", "surprised", "bored", "confused", ] SCENE_LABELS = [ "bedroom", "market", "office", "park", "library", "kitchen", "street", "gym", "beach", "forest", "restaurant", "school", "hospital", "bathroom", "church", "space", "rooftop", "farm", "living_room", # Sports venues "soccer_field", "cricket_ground", "basketball_court", "tennis_court", "baseball_field", "golf_course", "bowling_alley", # Education / institutional "classroom", "auditorium", "laboratory", # Culture / entertainment "museum", "art_gallery", "theater", "cinema", "concert_hall", "stadium", "zoo", "aquarium", # Nature / outdoor "greenhouse", "cave", "mountain", "desert", "waterfall", "vineyard", # Architectural / historic "cemetery", "castle", "mansion", "cottage", "cabin", "lighthouse", "temple", "monastery", # Transport hubs "airport", "train_station", "subway", "bridge", "parking_lot", "gas_station", # Civic "bank", "prison", "police_station", "fire_station", "courthouse", # Industrial "factory", "warehouse", "construction_site", "garage", # Home annexes "basement", "attic", "laundry_room", "pantry", # Fun / recreation "swimming_pool", "casino", "nightclub", "arcade", "spa", # Misc "barn", "salon", "bakery", "none", ] # Keyword shortcuts (deterministic, run before LLM). Dict iteration order # = priority order (Python preserves insertion order), so put the more # specific / less ambiguous labels first. ACTION_KEYWORDS: Dict[str, Tuple[str, ...]] = { # Motion "dancing": ("danc", "boogi", "groov", "salsa", "waltz", "ballet", "tango", "disco", "twerk", "breakdanc", "hiphop", "choreograph", "bop"), "running": ("run", "jog", "sprint", "dash", "race", "rush", "scamper", "gallop"), "walking": ("walk", "stroll", "stride", "march", "pace", "saunter", "amble", "wander", "trek", "hike", "promenade", "tread"), "jumping": ("jump", "leap", "hop", "bound", "bounc", "spring", "skip", "vault"), "kicking": ("kick", "punt", "karate"), "climbing": (), # alias placeholder (not implemented) "stretching": ("stretch", "yoga", "warm-up", "warmup", "limber", "flex ", "loosen"), # Combat "fighting": ("fight", "punch", "box", "brawl", "attack", "combat", "battle", "duel", "wrestle", "spar", "martial", "strike", "assault"), # Social / gesture "waving": ("wav", "hello", " hi ", "greet", "salute", "bye", "farewell", "goodbye", "hail", "beckon"), "clapping": ("clap", "applau", "ovation", "hand claps", "handclap"), # Consumption "eating": ("eat", "munch", "chew", "feast", "dine", "meal", "food ", "breakfast", "lunch", "dinner", "snack", "bite", "nibble", "tast"), "drinking": ("drink", "sip", "gulp", "beverage", "coffee", "tea ", "juice", "soda", "water bottle", "thirst", "chug"), # Mental / quiet "reading": ("read", "book", "stud", "novel", "magazine", "newspaper", "textbook", "literat", "peruse", "brows"), "thinking": ("think", "ponder", "wonder", "contemplat", "muse ", "reflect", "meditat", "imagin", "consider", "daydream"), "writing": ("writ", "scribbl", "sign ", "note ", "pen ", "pencil", "letter", "journal", "jot", "author"), "praying": ("pray", "worship", "kneel", "devot", "amen", "blessing", "rosary"), # Emotional expression (but as ACTIONS — can combine with emotions) "laughing": ("laugh", "giggl", "chuckl", "guffaw", "snicker", "cackl", "lol", "rofl"), "crying": ("cry", "weep", "sob", "tear", "bawl", "whimper", "wail"), # Sports + gaming (put specific sport keywords BEFORE the generic # "kicking" and "fighting" keywords so they win). "football": ("football", "soccer", "fifa", "penalty kick", "goalkeep"), "cricket": ("cricket", "=wicket", "=wickets", "batsman", "bowler", "ipl "), "basketball": ("basketball", "dribbl", "slam dunk", "=dunk", "=nba", "hoops"), "tennis": ("tennis", "racquet", "=racket", "=wimbledon", "tennis serve"), "baseball": ("baseball", "home run", "homerun", "=mlb", "softball", "pitcher mound"), "golf": ("golf", "putter", "=putt ", "=putts", "tee shot", "fairway", "caddy"), "bowling": ("bowling", "strike game", "bowling alley", "tenpin", "=pins"), "skateboarding": ("skateboard", "skate park", "kickflip", "=ollie", "skating rink", "ice skat"), "gaming": ("gaming", "video game", "videogame", "=console", "playstation", "=xbox", "nintendo", "esport", "controller", "gamepad", "=ps5", "=ps4"), # Body / motion # Note: use "bowing"/"bowed"/"bow down" (not bare "bow") so we don't # catch "bow and arrow" (archery) below. "bowing": ("bowing", "=bowed", "take a bow", "bow down"), "hugging": ("hug", "embrac", "cuddl"), "kissing": ("kiss", "smooch", "peck"), "handshake": ("handshake", "shake hand", "shaking hand"), "pointing": ("point ", "pointing", "=points", "indicat"), "climbing": ("climb", "scal", "ascend", "mountaineer"), "falling": ("fall", "falling", "tripp", "tumbl", "toppl"), "crawling": ("crawl", "=creep", "creeping"), "rolling": ("=roll", "rolling", "=rolls"), "juggling": ("juggl",), # Water / snow / board "swimming": ("swim", "freestyle", "breaststroke", "backstroke"), "diving": ("div", "divin", "plung"), "surfing": ("surf", "wave ride", "longboard wave"), "skating": ("skate", "roller skat", "ice skat", "skating rink"), "skiing": ("ski ", "skiing", "=skis", "slalom"), "biking": ("bik", "bicycl", "cycl", "=bike", "pedal"), # Vehicle "driving": ("driv", "steering wheel", "behind the wheel"), "riding_horse": ("horseback", "riding a horse", "on horseback", "equestrian"), "rowing": ("row ", "rowing", "oar ", "canoe"), # Involuntary / body care "sneezing": ("sneez", "achoo"), "coughing": ("cough", "hack ", "splutter"), "yawning": ("yawn",), "shivering": ("shiver", "shudder", "freezing cold"), "scratching": ("scratch", "itch"), "shaving": ("shav",), "brushing_teeth": ("brush teeth", "brushing teeth", "toothbrush", "dental"), "combing_hair": ("comb hair", "combing hair", "brush hair", "brushing hair"), "dressing": ("dress ", "dressing", "getting dressed", "put on"), # Load / force "carrying": ("carry", "hauling", "toting", "lug ", "lugging"), "lifting": ("lift ", "lifting", "deadlift", "bench press", "barbell", "dumbbell"), "pushing": ("push ", "pushing", "=pushes", "shove", "press forward"), "pulling": ("pull ", "pulling", "=pulls", "tug ", "tugging", "dragg"), "throwing": ("throw", "toss", "hurl", "chuck ", "pitch "), "catching": ("catch", "grabbing"), # Precision / craft "fishing": ("fish", "angling"), "shooting": ("shoot", "rifle", "gun ", "pistol", "firing "), "archery": ("archer", "=bow", "arrow", "crossbow"), "painting": ("paint ", "painting", "easel", "=paints"), "drawing": ("draw ", "drawing", "sketch", "doodl"), "sculpting": ("sculpt", "clay", "statue carv"), "photographing": ("photograph", "=photo", "camera", "snapshot", "taking a picture"), # Voice / teach "singing": ("sing ", "singing", "sung", "=sings", "croon"), "whistling": ("whistl",), "yelling": ("yell", "shout", "scream"), "teaching": ("teach", "lectur", "instruct"), "presenting": ("present ", "presenting", "keynote", "pitch to"), # Instruments "playing_guitar": ("guitar", "strum", "bass guitar", "ukulele"), "playing_piano": ("piano", "keyboard play", "keys play"), "playing_drums": ("drum", "drumkit", "drum kit", "percuss"), # Modern life "shopping": ("shopping", "grocery run", "shopping cart", "browsing aisles"), "texting": ("text", "texting", "sms", "whatsapp", "messag"), "meditating": ("meditat", "=zen", "mindful", "namaste"), # Chores / household (must come before the "working" catch-all) "vacuuming": ("vacuum", "vaccum", "vaccu", "vaccui", "hoover", "hoovering"), "sweeping": ("sweep", "broom"), "cooking": ("cook", "bake", "boil", "fry", "recipe", "stir ", "simmer", "chef", "prepar"), # Wash is about water + dishes/laundry specifically. "washing": ("wash", "dish", "scrub", "launder", "laundry", "rins"), # Generic tidy-up — put BEFORE washing so "cleaning" wins its own label. "cleaning": ("clean", "tidy", "tidying", "wipe", "wipin", "spray bottle"), "dusting": ("dust", "feather duster", "dusting"), "mopping": ("mop ", "mopping", "mop the"), "ironing": ("iron ", "ironing", "press shirt", "ironing board"), "polishing": ("polish", "buff", "shine the", "waxing"), "gardening": ("garden", " plant ", "watering plant", "trowel", "weed"), # Rest / idle "sleeping": ("sleep", "nap ", "slumber", "doz", "snor", " rest", "snooze", "shuteye"), "sitting": ("sit ", "sitting", "seat", "chair", "bench", "stool", "perch", "recline", "lounge", "squat", "cross-legged", "crouch"), "standing_idle": ("stand", "idle", "wait", "motionless", "still ", "pose", "upright"), # Default catch-all — most specific last so others win first "working": ("work", "typ", "comput", "keyboard", "laptop", "task", "labor", "busy ", "grind", "employ", "job ", "offic", "market", " cook", "shop"), } # Remove the placeholder "climbing" entry (kept for future extension). ACTION_KEYWORDS.pop("climbing", None) EMOTION_KEYWORDS: Dict[str, Tuple[str, ...]] = { "happy": ("happy", "joy", "joyful", "cheer", "glad", "smil", "delight", "merr", "content", "pleased", "gleeful", "jolly", "elat", "bliss"), "sad": ("sad", "cry", "depress", "down ", "grief", "sorrow", "gloom", "melanchol", "unhappy", "heartbroken", "miser", "blue ", "mourn", "tearful", "woeful"), "angry": ("angry", "mad ", "furious", "rage", "annoy", "irrit", "frustrat", "livid", "fuming", "hostile", "enrag", "outrag", "pissed", "seething", "grumpy"), "tired": ("tired", "exhaust", "weary", "drows", "fatigue", "worn", "drained", "spent", "sluggish", "lethargic"), "excited": ("excit", "thrill", "energ", "eager", "pumped", "stoked", "hyped", "ecstatic", "euphoric", "enthusiast"), "scared": ("scared", "afraid", "terrif", "frighten", "fear", "petrified", "panick", "horrif", "spook", "nervous", "anxious", "worried", "unease"), "surprised": ("surpris", "shock", "astonish", "amaz", "stun", "startl", "dumbfound", "flabbergast", "taken aback", "gasp"), "bored": ("bored", "boring", "dull", "uninterest", "apathetic", "listless", "tedious", "monoton"), "confused": ("confus", "puzzl", "perplex", "bewilder", "baffl", "mystif", "lost ", "unclear", "huh"), "neutral": ("neutral", "calm", "plain", "ordinary", "normal"), } SCENE_KEYWORDS: Dict[str, Tuple[str, ...]] = { # Specific & unambiguous first "living_room": ("living room", "livingroom", "living-room", "lounge", "sofa", "couch", "sitting room", "parlor", "parlour", "den ", "carpet", "tv room", "=rug", "=rugs", "=room", "=rooms"), "soccer_field": ("soccer field", "football field", "football pitch", "soccer pitch", "football ground", "=pitch"), "cricket_ground": ("cricket ground", "cricket field", "cricket pitch", "cricket stadium"), "basketball_court": ("basketball court", "basketball arena", "basketball stadium", "indoor court"), "tennis_court": ("tennis court", "clay court", "grass court"), "baseball_field": ("baseball field", "baseball diamond", "baseball park", "ballpark"), "golf_course": ("golf course", "golf club", "putting green", "=fairway"), "bowling_alley": ("bowling alley", "bowling lane", "bowling center", "tenpin alley"), # Education / institutional "classroom": ("classroom", "=class", "lecture hall"), "auditorium": ("auditorium", "assembly hall"), "laboratory": ("laboratory", "=lab ", "=labs"), # Culture / entertainment "museum": ("museum", "exhibit", "gallery museum"), "art_gallery": ("art gallery", "art museum", "=gallery", "=galleries"), "theater": ("theater", "theatre", "playhouse", "broadway"), "cinema": ("cinema", "movie theater", "=movies", "movie hall"), "concert_hall": ("concert hall", "philharmonic", "symphony hall"), "stadium": ("stadium", "arena ", "coliseum", "bleacher"), "zoo": ("=zoo", "=zoos"), "aquarium": ("aquarium", "fish tank", "oceanarium"), # Nature / outdoor "greenhouse": ("greenhouse", "conservatory"), "cave": ("=cave", "=caves", "cavern", "grotto"), "mountain": ("mountain", "=peak", "summit", "alpine", "hillside"), "desert": ("desert", "=dune", "=dunes", "sahara"), "waterfall": ("waterfall", "=falls", "cascade"), "vineyard": ("vineyard", "winery", "wine estate", "grapevine"), # Architectural / historic "cemetery": ("cemetery", "graveyard", "=grave", "tombstone"), "castle": ("castle", "fortress", "citadel"), "mansion": ("mansion", "manor ", "estate house", "chateau"), "cottage": ("cottage", "hut "), "cabin": ("=cabin", "=cabins", "log cabin"), "lighthouse": ("lighthouse",), "temple": ("temple", "shrine", "pagoda"), "monastery": ("monastery", "abbey", "convent"), # Transport hubs "airport": ("airport", "terminal ", "airstrip", "runway"), "train_station": ("train station", "railway station", "platform "), "subway": ("subway", "=metro", "underground rail", "tube "), "bridge": ("=bridge", "=bridges", "overpass"), "parking_lot": ("parking lot", "parking garage", "car park"), "gas_station": ("gas station", "petrol station", "service station"), # Civic "bank": ("=bank", "=banks", "vault "), "prison": ("prison", "=jail", "=jails", "penitentiary"), "police_station": ("police station", "=precinct", "cop shop"), "fire_station": ("fire station", "firehouse"), "courthouse": ("courthouse", "court room", "courtroom", "=court"), # Industrial "factory": ("factory", "plant floor", "assembly line", "manufacturing"), "warehouse": ("warehouse", "stockroom", "depot"), "construction_site": ("construction site", "building site", "scaffolding"), "garage": ("=garage", "=garages", "repair shop"), # Home annexes "basement": ("basement", "cellar"), "attic": ("=attic", "=attics", "loft "), "laundry_room": ("laundry room", "washer dryer"), "pantry": ("pantry", "larder"), # Fun / recreation "swimming_pool": ("swimming pool", "=pool", "=pools", "poolside"), "casino": ("casino", "roulette ", "slot machine"), "nightclub": ("nightclub", "=club ", "disco ", "rave "), "arcade": ("=arcade", "=arcades", "game hall"), "spa": ("=spa", "=spas", "massage parlor", "wellness center"), # Misc "barn": ("=barn", "=barns"), "salon": ("=salon", "barbershop", "hair salon", "beauty salon"), "bakery": ("bakery", "pastry shop", "patisserie"), "bedroom": ("bedroom", " bed ", " bed,", " bed.", "bedside", "pillow", "mattress", "dorm"), "bathroom": ("bathroom", "toilet", "shower", "washroom", "restroom", "lavatory", "sink ", "bathtub"), "kitchen": ("kitchen", "stove", "counter", " cook", "recipe", "cupboard", "fridge"), "library": ("library", "bookshelf", "shelves", "archive", "reading room"), "office": ("office", "cubicle", "meeting", "boardroom", "workplace", "workspace", "desk "), "school": ("school", "classroom", "university", "college", "lecture", "chalkboard", "blackboard", "student"), "hospital": ("hospital", "clinic", "infirmary", "icu", "ward ", "medical", "doctor", "nurse", "emergency room", "er "), "church": ("church", "cathedral", "chapel", "temple", "mosque", "synagogue", "altar", "pew"), "restaurant": ("restaurant", "cafe", "café", "diner", "bistro", "bar ", "pub", "canteen", "eatery"), "gym": ("gym", "fitness", "workout", "weight room", "studio ", "dojo", "stadium", "arena", "court "), "market": ("market", "bazaar", "stall", " shop", "store", "supermarket", "grocery"), "park": ("park", "garden", "playground", "lawn", "meadow"), "forest": ("forest", "woods", "jungle", "rainforest", "thicket", "grove", "bush "), "beach": ("beach", "seaside", "shore", "coast", "ocean", " sea ", "sandy", "sand ", "surf"), "street": ("street", "road", "sidewalk", "alley", "avenue", "boulevard", "city", "downtown", "intersection"), "rooftop": ("rooftop", "roof ", "terrace", "balcony"), "space": ("space", "cosmos", "galaxy", "moon ", "mars ", "planet", "spaceship", "astronaut", " orbit", "star "), "farm": ("farm", "barn", "pasture", "ranch", "countryside", "field ", "meadow", "hayloft"), } # --------------------------------------------------------------------------- # Prompt parsing # --------------------------------------------------------------------------- def _keyword_match(prompt_lc: str, table: Dict[str, Tuple[str, ...]]) -> str | None: """Match keywords against the prompt. Convention: a keyword starting with `=` is matched as a *whole word* (`\\b...\\b`). Everything else is a prefix match (`\\b...`), which lets us catch conjugations like 'danc' -> dance/dancing/danced. The `=word` form is used for short ambiguous words (e.g. =rug avoids matching 'rugged'). """ for label, keys in table.items(): for k in keys: if k.startswith("="): pattern = rf"\b{k[1:]}\b" else: pattern = rf"\b{k}" if re.search(pattern, prompt_lc): return label return None def _llm_classify(prompt: str, labels: list[str], hypothesis: str) -> Tuple[str, float]: """Zero-shot classification via local transformers pipeline. No network API.""" from transformers import pipeline # lazy: avoid import cost if keyword matched global _ZS_PIPE # noqa: PLW0603 if _ZS_PIPE is None: logger.info("Loading zero-shot classifier (facebook/bart-large-mnli, cached on first run)...") t0 = time.time() _ZS_PIPE = pipeline("zero-shot-classification", model="facebook/bart-large-mnli") logger.debug("Classifier loaded in %.1fs", time.time() - t0) out = _ZS_PIPE(prompt, candidate_labels=labels, hypothesis_template=hypothesis) top_label = out["labels"][0] top_score = float(out["scores"][0]) logger.debug("[llm] labels=%s scores=%s", out["labels"][:3], [round(s, 3) for s in out["scores"][:3]]) return top_label, top_score _ZS_PIPE = None # type: ignore[assignment] def parse_prompt(prompt: str) -> Tuple[str, str, str]: """Return (action, emotion, scene). Always canonical labels — never hallucinates.""" if not prompt or not prompt.strip(): raise ValueError("Prompt is empty.") prompt_lc = prompt.lower().strip() logger.debug("[parse] prompt=%r", prompt_lc) action = _keyword_match(prompt_lc, ACTION_KEYWORDS) emotion = _keyword_match(prompt_lc, EMOTION_KEYWORDS) scene = _keyword_match(prompt_lc, SCENE_KEYWORDS) logger.debug("[parse] keyword action=%s emotion=%s scene=%s", action, emotion, scene) # Thresholds are conservative: BART's softmax over a closed set always # produces *something*, and low-confidence wins are usually hallucinated # attributes the user never mentioned. Prefer the neutral fallback. if action is None: try: lbl, score = _llm_classify( prompt_lc, ACTION_LABELS, "This is a description of someone {}." ) action = lbl if score >= 0.35 else "standing_idle" logger.debug("[parse] llm-action=%s (score=%.3f -> %s)", lbl, score, action) except Exception as e: # model load / inference failure -> safe default logger.warning("[parse] LLM action classification failed (%s); defaulting to standing_idle", e) action = "standing_idle" if emotion is None: try: lbl, score = _llm_classify( prompt_lc, EMOTION_LABELS, "The emotional tone of this is {}." ) emotion = lbl if score >= 0.55 else "neutral" logger.debug("[parse] llm-emotion=%s (score=%.3f -> %s)", lbl, score, emotion) except Exception as e: logger.warning("[parse] LLM emotion classification failed (%s); defaulting to neutral", e) emotion = "neutral" if scene is None: try: # Only the real scenes — we add "none" manually if confidence is low. lbl, score = _llm_classify( prompt_lc, SCENE_LABELS[:-1], "The setting or location of this scene is a {}." ) scene = lbl if score >= 0.55 else "none" logger.debug("[parse] llm-scene=%s (score=%.3f -> %s)", lbl, score, scene) except Exception as e: logger.warning("[parse] LLM scene classification failed (%s); defaulting to none", e) scene = "none" assert action in ACTION_LABELS assert emotion in EMOTION_LABELS assert scene in SCENE_LABELS logger.info("[parse] resolved action=%s emotion=%s scene=%s", action, emotion, scene) return action, emotion, scene # --------------------------------------------------------------------------- # Action keyframe functions # --------------------------------------------------------------------------- def _copy_pose() -> Dict[int, Tuple[float, float]]: return {k: (v[0], v[1]) for k, v in REST_POSE.items()} def _shift(p: Dict[int, Tuple[float, float]], joint: int, dx: float, dy: float) -> None: x, y = p[joint] p[joint] = (x + dx, y + dy) def action_standing_idle(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() breath = 1.2 * math.sin(2 * math.pi * t) for j in (HEAD, NECK, TORSO, LSH, RSH): _shift(p, j, 0, -breath) return p def action_walking(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t # Leg swing (opposite phase) leg_amp = 28 _shift(p, LKN, 0, 0); _shift(p, LFT, 0, 0) _shift(p, RKN, 0, 0); _shift(p, RFT, 0, 0) p[LFT] = (p[LFT][0] + leg_amp * math.sin(phase), p[LFT][1] - abs(leg_amp * 0.35 * math.sin(phase))) p[RFT] = (p[RFT][0] + leg_amp * math.sin(phase + math.pi), p[RFT][1] - abs(leg_amp * 0.35 * math.sin(phase + math.pi))) p[LKN] = ((p[LHP][0] + p[LFT][0]) / 2 - 4, (p[LHP][1] + p[LFT][1]) / 2) p[RKN] = ((p[RHP][0] + p[RFT][0]) / 2 + 4, (p[RHP][1] + p[RFT][1]) / 2) # Arms counter-swing arm_amp = 22 p[LHA] = (p[LHA][0] + arm_amp * math.sin(phase + math.pi), p[LHA][1]) p[RHA] = (p[RHA][0] + arm_amp * math.sin(phase), p[RHA][1]) p[LEL] = ((p[LSH][0] + p[LHA][0]) / 2 - 2, (p[LSH][1] + p[LHA][1]) / 2) p[REL] = ((p[RSH][0] + p[RHA][0]) / 2 + 2, (p[RSH][1] + p[RHA][1]) / 2) # Vertical bob bob = 3 * abs(math.sin(phase * 2)) for j in (HEAD, NECK, TORSO, PELVIS, LSH, RSH, LHP, RHP): _shift(p, j, 0, -bob) return p def action_running(t: float) -> Dict[int, Tuple[float, float]]: p = action_walking(t) # Amplify by lifting knees/arms further; lean torso forward. for j in (HEAD, NECK, TORSO): _shift(p, j, 10, 0) # lean forward return p def action_dancing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t # Arms raised, waving up p[LSH] = (p[LSH][0], p[LSH][1]) p[RSH] = (p[RSH][0], p[RSH][1]) p[LEL] = (p[LSH][0] - 30 + 8 * math.sin(phase), p[LSH][1] - 20) p[LHA] = (p[LEL][0] - 10, p[LEL][1] - 60 + 12 * math.sin(phase)) p[REL] = (p[RSH][0] + 30 + 8 * math.sin(phase + math.pi), p[RSH][1] - 20) p[RHA] = (p[REL][0] + 10, p[REL][1] - 60 + 12 * math.sin(phase + math.pi)) # Hip sway sway = 10 * math.sin(phase) for j in (PELVIS, LHP, RHP, LKN, LFT, RKN, RFT): _shift(p, j, sway, 0) # Counter-sway upper body for j in (HEAD, NECK, TORSO, LSH, RSH): _shift(p, j, -sway * 0.4, 0) # Vertical bounce bounce = 6 * abs(math.sin(phase * 2)) for j in p: _shift(p, j, 0, -bounce) return p def action_reading(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() # Bend torso slightly forward _shift(p, HEAD, 0, 18) _shift(p, NECK, 0, 10) _shift(p, TORSO, 0, 5) # Hands in front of chest, slightly apart (holding a book) p[LEL] = (230, 230) p[LHA] = (240, 260) p[REL] = (282, 230) p[RHA] = (272, 260) # Subtle head nod nod = 2 * math.sin(2 * math.pi * t) _shift(p, HEAD, 0, nod) return p def action_waving(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t # Right arm raised, hand waving left-right p[RSH] = (p[RSH][0], p[RSH][1]) p[REL] = (p[RSH][0] + 40, p[RSH][1] - 40) p[RHA] = (p[REL][0] + 20 * math.sin(phase * 2), p[REL][1] - 60) # Slight head tilt right _shift(p, HEAD, 3, -2) return p def action_sitting(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() drop = 40 # Lower whole upper body for j in (HEAD, NECK, TORSO, PELVIS, LSH, RSH, LHP, RHP): _shift(p, j, 0, drop) # Knees forward, feet forward p[LKN] = (220, 360 + drop) p[RKN] = (292, 360 + drop) p[LFT] = (180, 380 + drop) p[RFT] = (330, 380 + drop) # Tiny sway sway = 1.5 * math.sin(2 * math.pi * t) for j in (HEAD, NECK, TORSO): _shift(p, j, sway, 0) return p def action_jumping(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t lift = max(0.0, 40 * math.sin(phase)) # Whole body rises/falls for j in p: _shift(p, j, 0, -lift) # Arms up p[LEL] = (p[LSH][0] - 15, p[LSH][1] - 35) p[LHA] = (p[LSH][0] - 5, p[LSH][1] - 85) p[REL] = (p[RSH][0] + 15, p[RSH][1] - 35) p[RHA] = (p[RSH][0] + 5, p[RSH][1] - 85) # Knees bent when in-air if lift > 8: _shift(p, LKN, 0, -12); _shift(p, RKN, 0, -12) _shift(p, LFT, 0, -18); _shift(p, RFT, 0, -18) return p def action_fighting(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t # Slight crouch + forward lean for j in (HEAD, NECK, TORSO): _shift(p, j, 6, 6) # Alternating punches punch_r = max(0.0, math.sin(phase)) punch_l = max(0.0, math.sin(phase + math.pi)) p[REL] = (p[RSH][0] + 20 + 10 * punch_r, p[RSH][1] + 5) p[RHA] = (p[RSH][0] + 40 + 40 * punch_r, p[RSH][1]) p[LEL] = (p[LSH][0] - 20 - 10 * punch_l, p[LSH][1] + 5) p[LHA] = (p[LSH][0] - 40 - 40 * punch_l, p[LSH][1]) return p def action_thinking(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() # Right hand near chin p[REL] = (280, 180) p[RHA] = (260, 140) # Head tilt tilt = 4 * math.sin(2 * math.pi * t) _shift(p, HEAD, tilt, 0) return p def action_working(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t * 2 # Lean forward for j in (HEAD, NECK, TORSO): _shift(p, j, 0, 8) # Hands out front, typing p[LEL] = (230, 230) p[REL] = (282, 230) p[LHA] = (235, 265 + 3 * math.sin(phase)) p[RHA] = (277, 265 + 3 * math.sin(phase + math.pi)) return p def action_eating(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t * 2 lift = 22 + 8 * math.sin(phase) # Right hand goes to mouth and back p[REL] = (p[HEAD][0] + 22, p[HEAD][1] + 18) p[RHA] = (p[HEAD][0] + 6, p[HEAD][1] + 6 - lift * 0.2) # Left hand holds something in front (bowl) p[LEL] = (230, 240) p[LHA] = (244, 268) return p def action_drinking(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t tilt = 6 * math.sin(phase) # Head tilts back slightly while drinking _shift(p, HEAD, 0, -2 - abs(tilt)) # Right hand to mouth holding "cup" p[REL] = (p[HEAD][0] + 18, p[HEAD][1] + 20) p[RHA] = (p[HEAD][0] + 4, p[HEAD][1] - 6) return p def action_sleeping(t: float) -> Dict[int, Tuple[float, float]]: # Lying down: rotate the rest pose 90° around the body's midpoint. breath = 1.2 * math.sin(2 * math.pi * t) cx, cy = 256.0, 400.0 rotated: Dict[int, Tuple[float, float]] = {} for j, (x, y) in REST_POSE.items(): # Rotate 90° counter-clockwise: (x,y) -> (y, -x), then translate. dx = x - 256.0 dy = y - 275.0 nx = cx + dy * 1.0 ny = cy - dx * 0.9 rotated[j] = (nx, ny + breath) return rotated def action_crying(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t * 3 sob = 3 * math.sin(phase) # Shoulders shake; body slumps _shift(p, HEAD, sob * 0.5, 6) _shift(p, NECK, sob * 0.3, 3) _shift(p, LSH, -sob - 2, 4) _shift(p, RSH, sob + 2, 4) # Hands to face p[LEL] = (p[HEAD][0] - 32, p[HEAD][1] + 15) p[LHA] = (p[HEAD][0] - 12, p[HEAD][1] + 4) p[REL] = (p[HEAD][0] + 32, p[HEAD][1] + 15) p[RHA] = (p[HEAD][0] + 12, p[HEAD][1] + 4) return p def action_laughing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t * 4 shake = 4 * math.sin(phase) # Head tilts back, body shakes _shift(p, HEAD, shake * 0.5, -4) _shift(p, NECK, shake * 0.3, -1) _shift(p, TORSO, shake * 0.4, 0) # Hand over belly / mouth p[REL] = (p[HEAD][0] + 20, p[HEAD][1] + 28) p[RHA] = (p[HEAD][0] + 5, p[HEAD][1] + 8) p[LEL] = (p[TORSO][0] - 30, p[TORSO][1] + 15) p[LHA] = (p[TORSO][0] - 10, p[TORSO][1] + 25) return p def action_praying(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() # Slight head bow _shift(p, HEAD, 0, 6 + 1.5 * math.sin(2 * math.pi * t)) _shift(p, NECK, 0, 3) # Hands pressed together in front of chest p[LEL] = (238, 210) p[REL] = (274, 210) p[LHA] = (254, 190) p[RHA] = (258, 190) return p def action_clapping(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t * 4 # Hands come together and apart offset = 12 + 14 * abs(math.cos(phase)) p[LEL] = (228, 215) p[REL] = (284, 215) cx = 256 p[LHA] = (cx - offset, 225) p[RHA] = (cx + offset, 225) # Slight body bounce bounce = 2 * abs(math.sin(phase)) for j in (HEAD, NECK, TORSO, PELVIS): _shift(p, j, 0, -bounce) return p def action_writing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t * 3 # Lean forward slightly for j in (HEAD, NECK, TORSO): _shift(p, j, 0, 8) # Left hand holds paper steady p[LEL] = (228, 240) p[LHA] = (238, 272) # Right hand makes small writing motion wx = 268 + 8 * math.sin(phase) wy = 272 + 4 * math.cos(phase * 2) p[REL] = (286, 240) p[RHA] = (wx, wy) return p def action_stretching(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t reach = 8 + 4 * math.sin(phase) # Arms overhead, reaching p[LEL] = (p[LSH][0] - 10, p[LSH][1] - 40) p[LHA] = (p[LSH][0] - 4, p[LSH][1] - 80 - reach) p[REL] = (p[RSH][0] + 10, p[RSH][1] - 40) p[RHA] = (p[RSH][0] + 4, p[RSH][1] - 80 - reach) # Body extension _shift(p, HEAD, 0, -3) return p def action_vacuuming(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t # Lean forward over the vacuum for j in (HEAD, NECK, TORSO): _shift(p, j, 10, 6) # Both hands grip an invisible handle in front of the body at waist level. # Handle slides side-to-side following `sway`. sway = 28 * math.sin(phase) grip_x = 256 + sway grip_y = 310 p[LEL] = (grip_x - 26, grip_y - 30) p[REL] = (grip_x + 26, grip_y - 30) p[LHA] = (grip_x - 10, grip_y) p[RHA] = (grip_x + 10, grip_y) return p def action_sweeping(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t # Similar to vacuuming but wider arc and slightly higher grip for j in (HEAD, NECK, TORSO): _shift(p, j, 8, 6) sway = 36 * math.sin(phase) grip_x = 256 + sway grip_y = 300 p[LEL] = (grip_x - 30, grip_y - 30) p[REL] = (grip_x + 10, grip_y - 50) p[LHA] = (grip_x - 10, grip_y) p[RHA] = (grip_x + 18, grip_y - 20) return p def action_cooking(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t * 2 # Lean slightly forward over the stove for j in (HEAD, NECK, TORSO): _shift(p, j, 0, 8) # Left hand holds pot handle (steady) p[LEL] = (228, 245) p[LHA] = (222, 290) # Right hand stirs in circular motion cx, cy = 276, 290 r = 10 p[REL] = (290, 245) p[RHA] = (cx + r * math.cos(phase), cy + r * math.sin(phase)) return p def action_washing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t * 3 # Bend forward at sink for j in (HEAD, NECK, TORSO): _shift(p, j, 0, 10) # Hands close together, scrubbing motion scrub = 6 * math.sin(phase) p[LEL] = (236, 250) p[REL] = (276, 250) p[LHA] = (248 + scrub, 290) p[RHA] = (264 - scrub, 290 + 3 * math.cos(phase)) return p def action_gardening(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t * 2 # Bent over / crouched for j in (HEAD, NECK, TORSO): _shift(p, j, 10, 30) _shift(p, PELVIS, 0, 20) # Knees bent (squat) p[LKN] = (228, 380) p[RKN] = (284, 380) p[LFT] = (222, 438) p[RFT] = (290, 438) # Hands down near the ground, small digging motion dig = 4 * math.sin(phase) p[LEL] = (240, 320) p[REL] = (290, 320) p[LHA] = (240, 400 + dig) p[RHA] = (290, 400 - dig) return p def action_kicking(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t kick = max(0.0, math.sin(phase)) # Right leg kicks forward + up p[RKN] = (p[RHP][0] + 30 * kick, p[RHP][1] + 60 - 30 * kick) p[RFT] = (p[RHP][0] + 70 * kick, p[RHP][1] + 80 - 70 * kick) # Arms balance (opposite) p[LEL] = (p[LSH][0] - 10, p[LSH][1] + 40 - 20 * kick) p[LHA] = (p[LSH][0] - 18, p[LSH][1] + 60 - 30 * kick) p[REL] = (p[RSH][0] + 10, p[RSH][1] + 20 + 15 * kick) p[RHA] = (p[RSH][0] + 25, p[RSH][1] + 10 + 25 * kick) return p def action_cleaning(t: float) -> Dict[int, Tuple[float, float]]: """Wiping a surface with spray + cloth. One hand sprays, other wipes.""" p = _copy_pose() phase = 2 * math.pi * t * 2 wipe = 18 * math.sin(phase) # Slight lean forward for j in (HEAD, NECK, TORSO): _shift(p, j, 0, 6) # Left hand holds the spray bottle (steady, raised) p[LEL] = (232, 230) p[LHA] = (226, 260) # Right hand wipes side-to-side p[REL] = (282, 230) p[RHA] = (282 + wipe, 258) return p def action_dusting(t: float) -> Dict[int, Tuple[float, float]]: """Feather-dusting a high shelf — one arm up, sweeping side-to-side.""" p = _copy_pose() phase = 2 * math.pi * t * 2 sweep = 16 * math.sin(phase) # Slight upward reach _shift(p, HEAD, 0, -2) # Right arm raised, dusting p[RSH] = (p[RSH][0], p[RSH][1]) p[REL] = (p[RSH][0] + 14, p[RSH][1] - 40) p[RHA] = (p[RSH][0] + 20 + sweep, p[RSH][1] - 80) # Left arm resting p[LEL] = (p[LSH][0] - 15, p[LSH][1] + 30) p[LHA] = (p[LSH][0] - 22, p[LSH][1] + 55) return p def action_mopping(t: float) -> Dict[int, Tuple[float, float]]: """Mopping the floor — similar stance to sweeping but a mop prop.""" p = _copy_pose() phase = 2 * math.pi * t for j in (HEAD, NECK, TORSO): _shift(p, j, 6, 8) sway = 32 * math.sin(phase) grip_x = 256 + sway grip_y = 302 p[LEL] = (grip_x - 28, grip_y - 28) p[REL] = (grip_x + 8, grip_y - 44) p[LHA] = (grip_x - 8, grip_y - 4) p[RHA] = (grip_x + 14, grip_y - 18) return p def action_ironing(t: float) -> Dict[int, Tuple[float, float]]: """Moving an iron back and forth on an ironing-board surface.""" p = _copy_pose() phase = 2 * math.pi * t * 2 slide = 20 * math.sin(phase) # Lean forward slightly for j in (HEAD, NECK, TORSO): _shift(p, j, 0, 6) # Right hand holds the iron and moves p[REL] = (266, 250) p[RHA] = (266 + slide, 288) # Left hand steadies the garment p[LEL] = (244, 250) p[LHA] = (238, 288) return p def action_polishing(t: float) -> Dict[int, Tuple[float, float]]: """Circular polishing motion on a surface with a cloth.""" p = _copy_pose() phase = 2 * math.pi * t * 2 for j in (HEAD, NECK, TORSO): _shift(p, j, 0, 6) # Right hand circles on a surface; left hand braces nearby cx, cy, r = 276, 288, 14 p[REL] = (278, 250) p[RHA] = (cx + r * math.cos(phase), cy + r * math.sin(phase)) p[LEL] = (238, 250) p[LHA] = (244, 288) return p def action_football(t: float) -> Dict[int, Tuple[float, float]]: """Soccer: plant foot + swinging kick leg, arms for balance.""" p = _copy_pose() phase = 2 * math.pi * t kick = max(0.0, math.sin(phase)) # Right leg swings forward for the kick p[RKN] = (p[RHP][0] + 20 * kick, p[RHP][1] + 60 - 20 * kick) p[RFT] = (p[RHP][0] + 55 * kick, p[RHP][1] + 85 - 40 * kick) # Left leg planted (slight bend) p[LKN] = (p[LHP][0] - 6, p[LHP][1] + 65) p[LFT] = (p[LHP][0] - 8, p[LHP][1] + 130) # Arms out for balance p[LEL] = (p[LSH][0] - 25, p[LSH][1] + 10) p[LHA] = (p[LSH][0] - 45, p[LSH][1] - 5) p[REL] = (p[RSH][0] + 25, p[RSH][1] + 20) p[RHA] = (p[RSH][0] + 40, p[RSH][1] + 40) # Slight lean back on kick for j in (HEAD, NECK, TORSO): _shift(p, j, -4 * kick, -2 * kick) return p def action_cricket(t: float) -> Dict[int, Tuple[float, float]]: """Cricket batting stance: bat held two-handed, slight body twist.""" p = _copy_pose() phase = 2 * math.pi * t swing = math.sin(phase) # Body sideways-ish (shift torso slightly) for j in (HEAD, NECK, TORSO): _shift(p, j, -4, 2) # Both hands together on the bat, near the right side p[LEL] = (250, 220) p[REL] = (275, 210) p[LHA] = (286 + 8 * swing, 235 - 6 * swing) p[RHA] = (296 + 8 * swing, 230 - 6 * swing) # Knees slightly bent (stance) p[LKN] = (p[LHP][0] - 2, p[LHP][1] + 70) p[RKN] = (p[RHP][0] + 2, p[RHP][1] + 70) return p def action_basketball(t: float) -> Dict[int, Tuple[float, float]]: """Dribbling: one hand pushes down on a bouncing ball.""" p = _copy_pose() phase = 2 * math.pi * t * 2 # Slight crouch, forward lean for j in (HEAD, NECK, TORSO): _shift(p, j, 6, 8) # Dribbling hand (right) bounces up and down push = abs(math.sin(phase)) p[REL] = (p[RSH][0] + 25, p[RSH][1] + 30) p[RHA] = (p[RSH][0] + 40, p[RSH][1] + 60 + 20 * push) # Left hand lower / guarding p[LEL] = (p[LSH][0] - 15, p[LSH][1] + 35) p[LHA] = (p[LSH][0] - 30, p[LSH][1] + 55) # Knees bent (athletic stance) p[LKN] = (p[LHP][0] - 4, p[LHP][1] + 60) p[RKN] = (p[RHP][0] + 4, p[RHP][1] + 60) return p def action_tennis(t: float) -> Dict[int, Tuple[float, float]]: """Forehand swing: racket arm swings across the body.""" p = _copy_pose() phase = 2 * math.pi * t # Swing phase: arm goes from cocked-back to forward swing = math.sin(phase) # -1 to 1 # Right arm carries the racket p[REL] = (p[RSH][0] + 20 + 15 * swing, p[RSH][1] + 10) p[RHA] = (p[RSH][0] + 40 + 40 * swing, p[RSH][1] - 10 - 15 * swing) # Left arm opposes for balance p[LEL] = (p[LSH][0] - 20 - 10 * swing, p[LSH][1] + 10) p[LHA] = (p[LSH][0] - 35 - 20 * swing, p[LSH][1] + 25) # Slight hip twist via a lateral shift for j in (PELVIS, LHP, RHP, LKN, RKN, LFT, RFT): _shift(p, j, 3 * swing, 0) return p def action_baseball(t: float) -> Dict[int, Tuple[float, float]]: """Batting: bat held over the shoulder, small wind-up motion.""" p = _copy_pose() phase = 2 * math.pi * t windup = math.sin(phase) * 0.4 # Body slightly coiled for j in (HEAD, NECK, TORSO): _shift(p, j, -3 + 4 * windup, 4) # Both hands grip bat near right shoulder p[LEL] = (270, 195) p[REL] = (290, 180) grip_x = 310 grip_y = 175 p[LHA] = (grip_x - 6, grip_y + 4) p[RHA] = (grip_x + 4, grip_y) # Feet apart stance p[LFT] = (p[LHP][0] - 12, 448) p[RFT] = (p[RHP][0] + 12, 448) p[LKN] = (p[LHP][0] - 6, 380) p[RKN] = (p[RHP][0] + 6, 380) return p def action_golf(t: float) -> Dict[int, Tuple[float, float]]: """Golf swing: club swings from behind to follow-through.""" p = _copy_pose() phase = 2 * math.pi * t swing = math.sin(phase) # -1 (backswing) to 1 (follow-through) # Bent over the ball for j in (HEAD, NECK, TORSO): _shift(p, j, 0, 10) # Hands together on the club base_x = 256 + 40 * swing base_y = 290 - 20 * abs(swing) p[LEL] = (base_x - 20, base_y - 40) p[REL] = (base_x - 5, base_y - 50) p[LHA] = (base_x - 2, base_y - 5) p[RHA] = (base_x + 6, base_y - 2) # Stance p[LFT] = (p[LHP][0] - 10, 448) p[RFT] = (p[RHP][0] + 10, 448) return p def action_bowling(t: float) -> Dict[int, Tuple[float, float]]: """Bowling release: right arm swings forward, body steps forward.""" p = _copy_pose() phase = 2 * math.pi * t swing = math.sin(phase) # Right arm swings from behind (swing<0) to forward (swing>0) p[REL] = (p[RSH][0] + 20 * swing, p[RSH][1] + 30 + 20 * abs(swing)) p[RHA] = (p[RSH][0] + 50 * swing, p[RSH][1] + 65 + 10 * swing) # Left arm raised for balance p[LEL] = (p[LSH][0] - 20, p[LSH][1] + 5) p[LHA] = (p[LSH][0] - 35, p[LSH][1] - 5) # Slight forward step for j in (HEAD, NECK, TORSO): _shift(p, j, 4 * swing, 2) return p def action_skateboarding(t: float) -> Dict[int, Tuple[float, float]]: """Riding a skateboard: balanced, slight crouch, one foot pushes.""" p = _copy_pose() phase = 2 * math.pi * t lean = 3 * math.sin(phase) # Crouched slightly for j in (HEAD, NECK, TORSO): _shift(p, j, lean, 6) # Arms out for balance p[LEL] = (p[LSH][0] - 25, p[LSH][1] + 10 + 3 * math.sin(phase)) p[LHA] = (p[LSH][0] - 45, p[LSH][1] + 5) p[REL] = (p[RSH][0] + 25, p[RSH][1] + 10 - 3 * math.sin(phase)) p[RHA] = (p[RSH][0] + 45, p[RSH][1] + 5) # Feet apart (on board), slight knee bend p[LFT] = (224, 440) p[RFT] = (288, 440) p[LKN] = (p[LHP][0] - 4, p[LHP][1] + 60) p[RKN] = (p[RHP][0] + 4, p[RHP][1] + 60) return p # --------------------------------------------------------------------------- # Extra actions (body/motion, water/snow, vehicles, body-care, craft, voice, # instruments, modern life). Each is a simple pose — distinctive props drawn # in draw_frame do most of the recognition work. # --------------------------------------------------------------------------- def action_bowing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() bow = 30 + 4 * math.sin(2 * math.pi * t) _shift(p, HEAD, 0, bow) _shift(p, NECK, 0, bow * 0.7) _shift(p, TORSO, 0, bow * 0.4) _shift(p, LSH, 0, bow * 0.5); _shift(p, RSH, 0, bow * 0.5) _shift(p, LEL, 6, bow * 0.6); _shift(p, REL, -6, bow * 0.6) _shift(p, LHA, 12, bow * 0.6); _shift(p, RHA, -12, bow * 0.6) return p def action_hugging(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() squeeze = 2 * math.sin(2 * math.pi * t * 2) p[LEL] = (226, 220) p[REL] = (286, 220) p[LHA] = (246 - squeeze, 230) p[RHA] = (266 + squeeze, 230) _shift(p, HEAD, 0, 4) return p def action_kissing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() _shift(p, HEAD, 6, 6) _shift(p, NECK, 3, 2) p[REL] = (p[HEAD][0] + 18, p[HEAD][1] + 10) p[RHA] = (p[HEAD][0] + 6, p[HEAD][1] + 2) return p def action_handshake(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() shake = 4 * math.sin(2 * math.pi * t * 3) p[REL] = (p[RSH][0] + 30, p[RSH][1] + 20) p[RHA] = (p[RSH][0] + 70, p[RSH][1] + 30 + shake) return p def action_pointing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() p[REL] = (p[RSH][0] + 35, p[RSH][1] + 10) p[RHA] = (p[RSH][0] + 95, p[RSH][1] + 4) return p def action_climbing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t up = math.sin(phase) # Left arm reaching up p[LEL] = (p[LSH][0] - 10, p[LSH][1] - 30 + 10 * up) p[LHA] = (p[LSH][0] - 6, p[LSH][1] - 80 + 10 * up) # Right arm pulling down p[REL] = (p[RSH][0] + 15, p[RSH][1] + 10 - 10 * up) p[RHA] = (p[RSH][0] + 10, p[RSH][1] + 50 - 10 * up) # Legs staggered p[LKN] = (p[LHP][0] - 4, p[LHP][1] + 40 + 10 * up) p[LFT] = (p[LHP][0] - 8, p[LHP][1] + 90 + 10 * up) p[RKN] = (p[RHP][0] + 6, p[RHP][1] + 70 - 10 * up) p[RFT] = (p[RHP][0] + 10, p[RHP][1] + 130 - 10 * up) return p def action_falling(t: float) -> Dict[int, Tuple[float, float]]: # Rotate entire body ~30° clockwise around pelvis. ang = math.radians(35 + 10 * math.sin(2 * math.pi * t)) cos, sin = math.cos(ang), math.sin(ang) cx, cy = 256.0, 310.0 rot: Dict[int, Tuple[float, float]] = {} for j, (x, y) in REST_POSE.items(): dx, dy = x - cx, y - cy rot[j] = (cx + dx * cos - dy * sin, cy + dx * sin + dy * cos) # Flail arms outward rot[LEL] = (rot[LSH][0] - 40, rot[LSH][1] + 10) rot[LHA] = (rot[LSH][0] - 70, rot[LSH][1] - 20) rot[REL] = (rot[RSH][0] + 40, rot[RSH][1] + 10) rot[RHA] = (rot[RSH][0] + 70, rot[RSH][1] - 20) return rot def action_crawling(t: float) -> Dict[int, Tuple[float, float]]: phase = 2 * math.pi * t * 2 # Horizontal body: rotate 90°, then override arms/legs with crawl cycle. ang = math.radians(90) cos, sin = math.cos(ang), math.sin(ang) cx, cy = 256.0, 380.0 p: Dict[int, Tuple[float, float]] = {} for j, (x, y) in REST_POSE.items(): dx, dy = x - 256.0, y - 275.0 p[j] = (cx + dx * cos - dy * sin, cy + dx * sin + dy * cos) # Alternate arm/leg reach reach = 18 * math.sin(phase) _shift(p, LHA, reach, 0); _shift(p, RHA, -reach, 0) _shift(p, LFT, -reach, 0); _shift(p, RFT, reach, 0) return p def action_rolling(t: float) -> Dict[int, Tuple[float, float]]: ang = 2 * math.pi * t cos, sin = math.cos(ang), math.sin(ang) cx, cy = 256.0, 340.0 r: Dict[int, Tuple[float, float]] = {} for j, (x, y) in REST_POSE.items(): dx, dy = x - 256.0, y - 275.0 r[j] = (cx + dx * cos - dy * sin, cy + dx * sin + dy * cos) return r def action_juggling(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() up = 2 * math.sin(2 * math.pi * t * 4) p[LEL] = (238, 250); p[LHA] = (234, 280 + up) p[REL] = (278, 250); p[RHA] = (282, 280 - up) return p def action_swimming(t: float) -> Dict[int, Tuple[float, float]]: # Horizontal body, arm windmill. ang = math.radians(90) cos, sin = math.cos(ang), math.sin(ang) cx, cy = 256.0, 350.0 p: Dict[int, Tuple[float, float]] = {} for j, (x, y) in REST_POSE.items(): dx, dy = x - 256.0, y - 275.0 p[j] = (cx + dx * cos - dy * sin, cy + dx * sin + dy * cos) phase = 2 * math.pi * t # Override arm tips to rotate for arm_sh, arm_el, arm_ha, off in ((LSH, LEL, LHA, 0.0), (RSH, REL, RHA, math.pi)): sx, sy = p[arm_sh] p[arm_el] = (sx + 20 * math.cos(phase + off), sy + 20 * math.sin(phase + off)) p[arm_ha] = (sx + 40 * math.cos(phase + off), sy + 40 * math.sin(phase + off)) return p def action_diving(t: float) -> Dict[int, Tuple[float, float]]: # Vertical head-down plunge. ang = math.radians(180) cos, sin = math.cos(ang), math.sin(ang) cx, cy = 256.0, 275.0 p: Dict[int, Tuple[float, float]] = {} for j, (x, y) in REST_POSE.items(): dx, dy = x - 256.0, y - 275.0 p[j] = (cx + dx * cos - dy * sin, cy + dx * sin + dy * cos) # Arms together overhead (which is now below) p[LEL] = (248, p[LSH][1] + 40) p[REL] = (264, p[RSH][1] + 40) p[LHA] = (252, p[LSH][1] + 80) p[RHA] = (260, p[RSH][1] + 80) # Slight upward bob bob = 6 * math.sin(2 * math.pi * t) for j in p: _shift(p, j, 0, -bob) return p def action_surfing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t lean = 4 * math.sin(phase) for j in (HEAD, NECK, TORSO): _shift(p, j, lean, 8) # Arms out wide p[LEL] = (p[LSH][0] - 30, p[LSH][1] + 5) p[LHA] = (p[LSH][0] - 55, p[LSH][1] + 10) p[REL] = (p[RSH][0] + 30, p[RSH][1] + 5) p[RHA] = (p[RSH][0] + 55, p[RSH][1] + 10) # Feet slightly apart & staggered p[LFT] = (224, 440); p[RFT] = (292, 440) return p def action_skating(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t glide = 15 * math.sin(phase) # Arms swinging opposite p[LHA] = (p[LHA][0] + glide, p[LHA][1]) p[RHA] = (p[RHA][0] - glide, p[RHA][1]) p[LEL] = ((p[LSH][0] + p[LHA][0]) / 2, (p[LSH][1] + p[LHA][1]) / 2) p[REL] = ((p[RSH][0] + p[RHA][0]) / 2, (p[RSH][1] + p[RHA][1]) / 2) # One foot out (gliding) p[RFT] = (p[RFT][0] + 20, p[RFT][1]) return p def action_skiing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() for j in (HEAD, NECK, TORSO): _shift(p, j, 4, 10) # Crouch p[LKN] = (p[LHP][0] - 4, p[LHP][1] + 50) p[RKN] = (p[RHP][0] + 4, p[RHP][1] + 50) p[LFT] = (p[LHP][0] - 10, p[LHP][1] + 110) p[RFT] = (p[RHP][0] + 10, p[RHP][1] + 110) # Arms back, holding poles p[LEL] = (p[LSH][0] - 20, p[LSH][1] + 30) p[REL] = (p[RSH][0] + 20, p[RSH][1] + 30) p[LHA] = (p[LSH][0] - 28, p[LSH][1] + 55) p[RHA] = (p[RSH][0] + 28, p[RSH][1] + 55) return p def action_biking(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t # Crouched, hands on handlebars for j in (HEAD, NECK, TORSO): _shift(p, j, 0, 20) p[LEL] = (244, 270); p[REL] = (268, 270) p[LHA] = (244, 300); p[RHA] = (268, 300) # Pedaling legs cx_l, cy_l = p[LHP][0] - 4, p[LHP][1] + 80 cx_r, cy_r = p[RHP][0] + 4, p[RHP][1] + 80 r = 30 p[LKN] = (cx_l + r * math.cos(phase), cy_l + r * math.sin(phase)) p[RKN] = (cx_r + r * math.cos(phase + math.pi), cy_r + r * math.sin(phase + math.pi)) p[LFT] = (cx_l + 40 * math.cos(phase), cy_l + 40 * math.sin(phase)) p[RFT] = (cx_r + 40 * math.cos(phase + math.pi), cy_r + 40 * math.sin(phase + math.pi)) return p def action_driving(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() drop = 30 for j in (HEAD, NECK, TORSO, PELVIS, LSH, RSH, LHP, RHP): _shift(p, j, 0, drop) # Hands on wheel p[LEL] = (236, 250 + drop); p[REL] = (276, 250 + drop) p[LHA] = (232, 290 + drop); p[RHA] = (280, 290 + drop) # Legs forward (seated) p[LKN] = (224, 380 + drop); p[RKN] = (288, 380 + drop) p[LFT] = (190, 410 + drop); p[RFT] = (322, 410 + drop) return p def action_riding_horse(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() bounce = 4 * abs(math.sin(2 * math.pi * t * 2)) drop = 20 for j in (HEAD, NECK, TORSO, PELVIS, LSH, RSH, LHP, RHP): _shift(p, j, 0, drop - bounce) # Hands holding reins p[LEL] = (236, 250 + drop); p[REL] = (276, 250 + drop) p[LHA] = (240, 285 + drop); p[RHA] = (272, 285 + drop) # Legs astride (bent outward) p[LKN] = (208, 380 + drop); p[RKN] = (304, 380 + drop) p[LFT] = (196, 440 + drop); p[RFT] = (316, 440 + drop) return p def action_rowing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t pull = 14 * math.sin(phase) drop = 20 for j in (HEAD, NECK, TORSO, PELVIS, LSH, RSH, LHP, RHP): _shift(p, j, 0, drop) p[LEL] = (232 + pull, 240 + drop); p[REL] = (280 - pull, 240 + drop) p[LHA] = (220 + pull * 2, 280 + drop) p[RHA] = (292 - pull * 2, 280 + drop) # Legs forward p[LKN] = (220, 370 + drop); p[RKN] = (292, 370 + drop) p[LFT] = (200, 420 + drop); p[RFT] = (312, 420 + drop) return p def action_sneezing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() jerk = 6 * math.sin(2 * math.pi * t * 4) _shift(p, HEAD, 0, 6 + jerk) _shift(p, NECK, 0, 3) p[REL] = (p[HEAD][0] + 20, p[HEAD][1] + 14) p[RHA] = (p[HEAD][0] + 6, p[HEAD][1] + 2) return p def action_coughing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() hunch = 4 * abs(math.sin(2 * math.pi * t * 3)) for j in (HEAD, NECK, TORSO): _shift(p, j, 0, 4 + hunch) p[REL] = (p[HEAD][0] + 18, p[HEAD][1] + 16) p[RHA] = (p[HEAD][0] + 4, p[HEAD][1] + 6) return p def action_yawning(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() _shift(p, HEAD, 0, -6) p[REL] = (p[HEAD][0] + 22, p[HEAD][1] + 18) p[RHA] = (p[HEAD][0] + 4, p[HEAD][1] - 2) # Stretched arms p[LEL] = (p[LSH][0] - 15, p[LSH][1] - 20) p[LHA] = (p[LSH][0] - 25, p[LSH][1] - 60) return p def action_shivering(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() tx = 2 * math.sin(2 * math.pi * t * 10) ty = 1.5 * math.cos(2 * math.pi * t * 10) for j in p: _shift(p, j, tx, ty) # Arms hug chest p[LEL] = (p[LSH][0] - 10, p[LSH][1] + 30) p[LHA] = (265, p[LSH][1] + 55) p[REL] = (p[RSH][0] + 10, p[RSH][1] + 30) p[RHA] = (247, p[RSH][1] + 55) return p def action_scratching(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() itch = 2 * math.sin(2 * math.pi * t * 5) p[REL] = (p[HEAD][0] + 14, p[HEAD][1] - 8) p[RHA] = (p[HEAD][0] + 6 + itch, p[HEAD][1] - 18) return p def action_shaving(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() stroke = 4 * math.sin(2 * math.pi * t * 3) p[REL] = (p[HEAD][0] + 24, p[HEAD][1] + 10) p[RHA] = (p[HEAD][0] + 18, p[HEAD][1] + stroke) _shift(p, HEAD, 0, 2) return p def action_brushing_teeth(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() brush = 6 * math.sin(2 * math.pi * t * 4) p[REL] = (p[HEAD][0] + 24, p[HEAD][1] + 18) p[RHA] = (p[HEAD][0] + 10 + brush, p[HEAD][1] + 8) return p def action_combing_hair(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() stroke = 12 * math.sin(2 * math.pi * t) p[REL] = (p[HEAD][0] + 18, p[HEAD][1] - 16) p[RHA] = (p[HEAD][0] + 6, p[HEAD][1] - 36 + stroke) return p def action_dressing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() pull = 8 * math.sin(2 * math.pi * t) p[LEL] = (p[LSH][0] - 10, p[LSH][1] - 25) p[LHA] = (p[HEAD][0] - 10, p[HEAD][1] - 10 - pull) p[REL] = (p[RSH][0] + 10, p[RSH][1] - 25) p[RHA] = (p[HEAD][0] + 10, p[HEAD][1] - 10 - pull) return p def action_carrying(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() sway = 1.5 * math.sin(2 * math.pi * t) for j in (HEAD, NECK, TORSO): _shift(p, j, sway, 4) p[LEL] = (p[LSH][0] - 5, p[LSH][1] + 40) p[REL] = (p[RSH][0] + 5, p[RSH][1] + 40) p[LHA] = (226, p[LSH][1] + 70) p[RHA] = (286, p[RSH][1] + 70) return p def action_lifting(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t lift = math.sin(phase) # Arms overhead p[LEL] = (p[LSH][0] - 10, p[LSH][1] - 30) p[LHA] = (p[LSH][0] - 18, p[LSH][1] - 80) p[REL] = (p[RSH][0] + 10, p[RSH][1] - 30) p[RHA] = (p[RSH][0] + 18, p[RSH][1] - 80) # Crouch depth varies p[LKN] = (p[LHP][0] - 6, p[LHP][1] + 40 + 10 * lift) p[RKN] = (p[RHP][0] + 6, p[RHP][1] + 40 + 10 * lift) p[LFT] = (p[LHP][0] - 12, 448) p[RFT] = (p[RHP][0] + 12, 448) return p def action_pushing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() for j in (HEAD, NECK, TORSO): _shift(p, j, 10, 4) # Both arms extended forward p[LEL] = (p[LSH][0] + 20, p[LSH][1] + 5) p[REL] = (p[RSH][0] + 20, p[RSH][1] + 5) p[LHA] = (p[LSH][0] + 50, p[LSH][1] + 10) p[RHA] = (p[RSH][0] + 50, p[RSH][1] + 10) # Feet staggered p[RFT] = (p[RHP][0] + 10, 444) return p def action_pulling(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() for j in (HEAD, NECK, TORSO): _shift(p, j, -8, 0) # Both arms pulled back p[LEL] = (p[LSH][0] - 20, p[LSH][1] + 20) p[REL] = (p[RSH][0] - 10, p[RSH][1] + 20) p[LHA] = (p[LSH][0] + 10, p[LSH][1] + 45) p[RHA] = (p[RSH][0] + 14, p[RSH][1] + 45) return p def action_throwing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() phase = 2 * math.pi * t fwd = math.sin(phase) p[REL] = (p[RSH][0] + 10 + 20 * fwd, p[RSH][1] + 5 - 10 * fwd) p[RHA] = (p[RSH][0] + 30 + 50 * fwd, p[RSH][1] - 5 - 25 * fwd) p[LEL] = (p[LSH][0] - 25, p[LSH][1] + 20) p[LHA] = (p[LSH][0] - 40, p[LSH][1] + 30) return p def action_catching(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() rise = 6 * math.sin(2 * math.pi * t) p[LEL] = (p[LSH][0] - 15, p[LSH][1] - 20 + rise) p[LHA] = (p[LSH][0] - 10, p[LSH][1] - 55 + rise) p[REL] = (p[RSH][0] + 15, p[RSH][1] - 20 + rise) p[RHA] = (p[RSH][0] + 10, p[RSH][1] - 55 + rise) return p def action_fishing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() drop = 30 for j in (HEAD, NECK, TORSO, PELVIS, LSH, RSH, LHP, RHP): _shift(p, j, 0, drop) # Seated legs p[LKN] = (224, 380 + drop); p[RKN] = (288, 380 + drop) p[LFT] = (194, 410 + drop); p[RFT] = (318, 410 + drop) # Hold rod in right hand extended forward p[REL] = (p[RSH][0] + 20, p[RSH][1] + 10 + drop) p[RHA] = (p[RSH][0] + 60, p[RSH][1] + drop) p[LEL] = (p[LSH][0] - 5, p[LSH][1] + 35 + drop) p[LHA] = (p[LSH][0] + 15, p[LSH][1] + 60 + drop) return p def action_shooting(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() for j in (HEAD, NECK, TORSO): _shift(p, j, 6, 6) p[REL] = (p[RSH][0] + 25, p[RSH][1] + 10) p[RHA] = (p[RSH][0] + 60, p[RSH][1] + 10) p[LEL] = (p[LSH][0] + 5, p[LSH][1] + 20) p[LHA] = (p[LSH][0] + 40, p[LSH][1] + 20) return p def action_archery(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() pull = 4 * math.sin(2 * math.pi * t) p[LEL] = (p[LSH][0] - 22, p[LSH][1] + 5) p[LHA] = (p[LSH][0] - 58, p[LSH][1] + 5) p[REL] = (p[RSH][0] - 10 + pull, p[RSH][1] + 15) p[RHA] = (p[HEAD][0] - 14 + pull, p[HEAD][1] + 20) return p def action_painting(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() stroke = 8 * math.sin(2 * math.pi * t * 2) # Left hand holds palette p[LEL] = (p[LSH][0] - 20, p[LSH][1] + 25) p[LHA] = (p[LSH][0] - 30, p[LSH][1] + 55) # Right brushes on easel p[REL] = (p[RSH][0] + 20, p[RSH][1] + 5) p[RHA] = (p[RSH][0] + 50 + stroke * 0.2, p[RSH][1] + 0 + stroke) return p def action_drawing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() for j in (HEAD, NECK, TORSO): _shift(p, j, 0, 6) tiny = 3 * math.sin(2 * math.pi * t * 4) p[LEL] = (230, 240); p[LHA] = (228, 270) p[REL] = (278, 250) p[RHA] = (270 + tiny, 280 + tiny * 0.5) return p def action_sculpting(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() press = 3 * math.sin(2 * math.pi * t * 3) p[LEL] = (234, 230); p[REL] = (278, 230) p[LHA] = (248 + press, 270); p[RHA] = (264 - press, 270) return p def action_photographing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() p[LEL] = (238, 200); p[REL] = (274, 200) p[LHA] = (242, p[HEAD][1] + 14) p[RHA] = (270, p[HEAD][1] + 14) return p def action_singing(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() _shift(p, HEAD, 0, -6) p[REL] = (p[HEAD][0] + 20, p[HEAD][1] + 14) p[RHA] = (p[HEAD][0] + 6, p[HEAD][1] + 4) p[LEL] = (p[LSH][0] - 22, p[LSH][1] - 10) p[LHA] = (p[LSH][0] - 35, p[LSH][1] - 30) return p def action_whistling(t: float) -> Dict[int, Tuple[float, float]]: p = action_standing_idle(t) _shift(p, HEAD, 0, -2) return p def action_yelling(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() _shift(p, HEAD, 0, 2) # Cupped hands around mouth p[LEL] = (p[HEAD][0] - 22, p[HEAD][1] + 8) p[REL] = (p[HEAD][0] + 22, p[HEAD][1] + 8) p[LHA] = (p[HEAD][0] - 10, p[HEAD][1] + 16) p[RHA] = (p[HEAD][0] + 10, p[HEAD][1] + 16) return p def action_teaching(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() p[REL] = (p[RSH][0] + 25, p[RSH][1] - 5) p[RHA] = (p[RSH][0] + 70, p[RSH][1] - 30) p[LEL] = (p[LSH][0] - 20, p[LSH][1] + 20) p[LHA] = (p[LSH][0] - 30, p[LSH][1] + 45) return p def action_presenting(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() swing = 4 * math.sin(2 * math.pi * t) p[REL] = (p[RSH][0] + 30, p[RSH][1] + 10 + swing) p[RHA] = (p[RSH][0] + 80, p[RSH][1] + 25 + swing) return p def action_playing_guitar(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() drop = 20 for j in (HEAD, NECK, TORSO, PELVIS, LSH, RSH, LHP, RHP): _shift(p, j, 0, drop) strum = 10 * math.sin(2 * math.pi * t * 3) # Right hand strums low; left hand on neck p[LEL] = (218, 250 + drop); p[LHA] = (200, 290 + drop) p[REL] = (278, 270 + drop) p[RHA] = (286, 300 + drop + strum) return p def action_playing_piano(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() drop = 25 for j in (HEAD, NECK, TORSO, PELVIS, LSH, RSH, LHP, RHP): _shift(p, j, 0, drop) press = 3 * math.sin(2 * math.pi * t * 4) p[LEL] = (228, 250 + drop); p[REL] = (282, 250 + drop) p[LHA] = (220, 290 + drop + press) p[RHA] = (288, 290 + drop - press) return p def action_playing_drums(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() drop = 25 for j in (HEAD, NECK, TORSO, PELVIS, LSH, RSH, LHP, RHP): _shift(p, j, 0, drop) phase = 2 * math.pi * t * 4 l = 8 * math.sin(phase) r = 8 * math.sin(phase + math.pi) p[LEL] = (230, 240 + drop) p[REL] = (282, 240 + drop) p[LHA] = (218, 285 + drop + l) p[RHA] = (294, 285 + drop + r) return p def action_shopping(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() for j in (HEAD, NECK, TORSO): _shift(p, j, 8, 4) p[LEL] = (246, 260); p[REL] = (272, 260) p[LHA] = (252, 300); p[RHA] = (268, 300) return p def action_texting(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() _shift(p, HEAD, 0, 10) _shift(p, NECK, 0, 4) tap = 1.5 * math.sin(2 * math.pi * t * 6) p[LEL] = (238, 240); p[REL] = (274, 240) p[LHA] = (248, 268 + tap) p[RHA] = (264, 268 - tap) return p def action_meditating(t: float) -> Dict[int, Tuple[float, float]]: p = _copy_pose() drop = 35 for j in (HEAD, NECK, TORSO, PELVIS, LSH, RSH): _shift(p, j, 0, drop) # Cross-legged p[LHP] = (232, 330); p[RHP] = (280, 330) p[LKN] = (190, 370); p[RKN] = (322, 370) p[LFT] = (250, 380); p[RFT] = (262, 380) # Hands on knees p[LEL] = (218, 280 + drop); p[REL] = (294, 280 + drop) p[LHA] = (200, 345); p[RHA] = (312, 345) return p def action_gaming(t: float) -> Dict[int, Tuple[float, float]]: """Video gaming: seated, controller in both hands, leaning toward screen.""" p = _copy_pose() phase = 2 * math.pi * t * 3 # Seated lower drop = 30 for j in (HEAD, NECK, TORSO, PELVIS, LSH, RSH, LHP, RHP): _shift(p, j, 0, drop) # Head forward toward screen _shift(p, HEAD, 0, 6) # Hands together in front of body (controller) p[LEL] = (238, 270 + drop) p[REL] = (274, 270 + drop) p[LHA] = (248 + math.sin(phase), 300 + drop) p[RHA] = (264 - math.sin(phase), 300 + drop) # Knees forward (sitting on couch) p[LKN] = (220, 380 + drop) p[RKN] = (292, 380 + drop) p[LFT] = (180, 400 + drop) p[RFT] = (330, 400 + drop) return p ACTIONS: Dict[str, Callable[[float], Dict[int, Tuple[float, float]]]] = { "standing_idle": action_standing_idle, "walking": action_walking, "running": action_running, "dancing": action_dancing, "reading": action_reading, "waving": action_waving, "sitting": action_sitting, "jumping": action_jumping, "fighting": action_fighting, "thinking": action_thinking, "working": action_working, "eating": action_eating, "drinking": action_drinking, "sleeping": action_sleeping, "crying": action_crying, "laughing": action_laughing, "praying": action_praying, "clapping": action_clapping, "writing": action_writing, "stretching": action_stretching, "kicking": action_kicking, "vacuuming": action_vacuuming, "sweeping": action_sweeping, "cooking": action_cooking, "washing": action_washing, "gardening": action_gardening, "cleaning": action_cleaning, "dusting": action_dusting, "mopping": action_mopping, "ironing": action_ironing, "polishing": action_polishing, "football": action_football, "cricket": action_cricket, "basketball": action_basketball, "tennis": action_tennis, "baseball": action_baseball, "golf": action_golf, "bowling": action_bowling, "skateboarding": action_skateboarding, "gaming": action_gaming, # Body / motion "bowing": action_bowing, "hugging": action_hugging, "kissing": action_kissing, "handshake": action_handshake, "pointing": action_pointing, "climbing": action_climbing, "falling": action_falling, "crawling": action_crawling, "rolling": action_rolling, "juggling": action_juggling, # Water / snow / board "swimming": action_swimming, "diving": action_diving, "surfing": action_surfing, "skating": action_skating, "skiing": action_skiing, "biking": action_biking, # Vehicle "driving": action_driving, "riding_horse": action_riding_horse, "rowing": action_rowing, # Involuntary / body care "sneezing": action_sneezing, "coughing": action_coughing, "yawning": action_yawning, "shivering": action_shivering, "scratching": action_scratching, "shaving": action_shaving, "brushing_teeth": action_brushing_teeth, "combing_hair": action_combing_hair, "dressing": action_dressing, # Load / force "carrying": action_carrying, "lifting": action_lifting, "pushing": action_pushing, "pulling": action_pulling, "throwing": action_throwing, "catching": action_catching, # Precision / craft "fishing": action_fishing, "shooting": action_shooting, "archery": action_archery, "painting": action_painting, "drawing": action_drawing, "sculpting": action_sculpting, "photographing": action_photographing, # Voice / teach "singing": action_singing, "whistling": action_whistling, "yelling": action_yelling, "teaching": action_teaching, "presenting": action_presenting, # Instruments "playing_guitar": action_playing_guitar, "playing_piano": action_playing_piano, "playing_drums": action_playing_drums, # Modern life "shopping": action_shopping, "texting": action_texting, "meditating": action_meditating, } # --------------------------------------------------------------------------- # Emotion post-transform # --------------------------------------------------------------------------- def apply_emotion( joints: Dict[int, Tuple[float, float]], emotion: str, t: float, ) -> Dict[int, Tuple[float, float]]: p = {k: v for k, v in joints.items()} if emotion == "sad": _shift(p, HEAD, 0, 8) _shift(p, NECK, 0, 4) _shift(p, LSH, -4, 8); _shift(p, RSH, 4, 8) _shift(p, LEL, -2, 10); _shift(p, REL, 2, 10) _shift(p, LHA, -2, 12); _shift(p, RHA, 2, 12) elif emotion == "happy": _shift(p, HEAD, 0, -3) bounce = 3 * abs(math.sin(2 * math.pi * t * 2)) for j in p: _shift(p, j, 0, -bounce) elif emotion == "angry": for j in (HEAD, NECK, TORSO): _shift(p, j, 5, 2) _shift(p, LSH, -2, -2); _shift(p, RSH, 2, -2) elif emotion == "tired": for j in p: # dampen — pull halfway toward rest pose rx, ry = REST_POSE[j] x, y = p[j] p[j] = (rx + (x - rx) * 0.5, ry + (y - ry) * 0.5) _shift(p, HEAD, 0, 10) _shift(p, NECK, 0, 4) elif emotion == "excited": bounce = 6 * abs(math.sin(2 * math.pi * t * 3)) for j in p: _shift(p, j, 0, -bounce) elif emotion == "scared": # Trembling + slight backward lean, arms pulled inward/up tremble_x = 2.2 * math.sin(2 * math.pi * t * 8) tremble_y = 1.4 * math.cos(2 * math.pi * t * 8) for j in p: _shift(p, j, tremble_x, tremble_y) _shift(p, HEAD, 0, -2) _shift(p, LSH, 2, -3); _shift(p, RSH, -2, -3) _shift(p, LEL, 4, -6); _shift(p, REL, -4, -6) _shift(p, LHA, 8, -10); _shift(p, RHA, -8, -10) elif emotion == "surprised": # Slight backward lean + arms raised outward for j in (HEAD, NECK, TORSO): _shift(p, j, -3, -2) _shift(p, LEL, -6, -8); _shift(p, REL, 6, -8) _shift(p, LHA, -12, -20); _shift(p, RHA, 12, -20) elif emotion == "bored": # Saggy: head down, shoulders drooping, minimal motion for j in p: # dampen rx, ry = REST_POSE[j] x, y = p[j] p[j] = (rx + (x - rx) * 0.6, ry + (y - ry) * 0.6) _shift(p, HEAD, 4, 10) _shift(p, NECK, 2, 5) _shift(p, LSH, -3, 5); _shift(p, RSH, 3, 5) elif emotion == "confused": # Head tilt (sideways), one shoulder raised tilt = 6 _shift(p, HEAD, tilt, 2) _shift(p, NECK, tilt * 0.4, 1) _shift(p, RSH, 0, -4) # Hand scratching head _shift(p, REL, 0, -20); _shift(p, RHA, -20, -30) # neutral: identity return p # --------------------------------------------------------------------------- # Rendering # --------------------------------------------------------------------------- def _draw_floor_line(draw: ImageDraw.ImageDraw, color: Tuple[int, int, int]) -> None: draw.line((0, FLOOR_Y, CANVAS, FLOOR_Y), fill=color, width=2) def _draw_scene_bedroom(draw: ImageDraw.ImageDraw) -> None: wall = (30, 24, 52) floor = (68, 45, 32) draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=wall) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=floor) # Floorboards for y in range(FLOOR_Y + 12, CANVAS, 16): draw.line((0, y, CANVAS, y), fill=(50, 30, 22), width=1) # Window (back-left) with curtains draw.rectangle((60, 80, 180, 210), outline=(180, 190, 210), width=3, fill=(30, 45, 90)) draw.line((120, 80, 120, 210), fill=(180, 190, 210), width=2) draw.line((60, 145, 180, 145), fill=(180, 190, 210), width=2) # Curtains draw.polygon([(36, 70), (60, 80), (60, 220), (40, 230)], fill=(130, 50, 60)) draw.polygon([(200, 70), (180, 80), (180, 220), (204, 230)], fill=(130, 50, 60)) # Moon through window draw.ellipse((135, 100, 170, 135), fill=(240, 230, 200)) # --- Bed (right side) --- bed_x1, bed_y1, bed_x2, bed_y2 = 300, 400, 504, 458 # Headboard (tall, behind the pillow) draw.rectangle((bed_x2 - 18, 330, bed_x2 + 6, bed_y1 + 10), fill=(90, 55, 30), outline=(160, 110, 70), width=2) # Decorative headboard curve draw.arc((bed_x2 - 22, 318, bed_x2 + 10, 350), start=180, end=360, fill=(200, 150, 100), width=3) # Footboard draw.rectangle((bed_x1 - 4, 395, bed_x1 + 10, bed_y2 + 8), fill=(90, 55, 30), outline=(160, 110, 70), width=2) # Bed frame draw.rectangle((bed_x1, bed_y1 + 14, bed_x2, bed_y2 + 8), fill=(80, 50, 28), outline=(160, 110, 70), width=2) # Mattress layer (visible as a stripe under the blanket) draw.rectangle((bed_x1 + 4, bed_y1 + 6, bed_x2 - 4, bed_y1 + 16), fill=(230, 225, 210), outline=(180, 170, 150), width=1) # Blanket (covering most of the mattress) draw.rectangle((bed_x1, bed_y1, bed_x2 - 4, bed_y1 + 28), fill=(120, 50, 80), outline=(180, 100, 130), width=2) # Blanket top edge (folded-over cuff) draw.rectangle((bed_x1 + 78, bed_y1 - 4, bed_x2 - 4, bed_y1 + 8), fill=(200, 200, 215), outline=(140, 140, 160), width=1) # Pillows (two, stacked at headboard end) draw.rectangle((bed_x2 - 82, bed_y1 - 12, bed_x2 - 20, bed_y1 + 10), fill=(245, 245, 250), outline=(150, 150, 165), width=2) draw.rectangle((bed_x2 - 82, bed_y1 - 14, bed_x2 - 20, bed_y1 - 6), fill=(220, 220, 230), outline=(150, 150, 165), width=1) # --- Nightstand --- ns_x1, ns_x2 = 240, 290 draw.rectangle((ns_x1, 408, ns_x2, 462), fill=(72, 48, 30), outline=(150, 110, 70), width=2) # Drawer draw.line((ns_x1, 432, ns_x2, 432), fill=(150, 110, 70), width=2) draw.ellipse(((ns_x1 + ns_x2) // 2 - 3, 418, (ns_x1 + ns_x2) // 2 + 3, 424), fill=(200, 170, 100)) # --- Lamp on the nightstand --- lamp_cx = (ns_x1 + ns_x2) // 2 lamp_base_y = 408 # Base draw.rectangle((lamp_cx - 14, lamp_base_y - 8, lamp_cx + 14, lamp_base_y), fill=(180, 150, 90), outline=(100, 80, 40), width=1) # Stem draw.line((lamp_cx, lamp_base_y - 8, lamp_cx, lamp_base_y - 40), fill=(200, 180, 120), width=3) # Shade (trapezoid) draw.polygon([(lamp_cx - 22, lamp_base_y - 40), (lamp_cx + 22, lamp_base_y - 40), (lamp_cx + 30, lamp_base_y - 76), (lamp_cx - 30, lamp_base_y - 76)], fill=(240, 210, 140), outline=(180, 140, 60)) # Warm glow under the shade draw.ellipse((lamp_cx - 42, lamp_base_y - 46, lamp_cx + 42, lamp_base_y - 18), fill=None, outline=(250, 220, 140), width=1) # --- Rug at the foot of the bed --- draw.rectangle((160, 475, 360, 505), fill=(100, 60, 60), outline=(200, 160, 110), width=2) # Tassels (short vertical ticks) at each short edge for tx in (160, 358): for dx in (-4, -1, 2, 5): draw.line((tx + dx, 505, tx + dx, 512), fill=(220, 180, 120), width=1) # Inner decoration for y in (484, 496): draw.line((175, y, 345, y), fill=(200, 160, 110), width=1) def _draw_scene_market(draw: ImageDraw.ImageDraw) -> None: sky = (60, 80, 110) ground = (55, 45, 30) draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=sky) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=ground) # Stall posts for x in (60, 180, 340, 460): draw.line((x, 200, x, FLOOR_Y), fill=(120, 80, 50), width=4) # Striped awnings stripes = [(180, 60, 60), (230, 220, 200)] for i, (x1, x2) in enumerate(((40, 200), (320, 480))): for k in range(8): c = stripes[k % 2] sx1 = x1 + (x2 - x1) * k // 8 sx2 = x1 + (x2 - x1) * (k + 1) // 8 draw.polygon([(sx1, 180), (sx2, 180), (sx2, 210), (sx1, 210)], fill=c) # Stall table + fruit circles draw.rectangle((40, 360, 200, 410), fill=(90, 60, 40), outline=(150, 110, 70), width=2) for cx, cy, col in ((70, 355, (220, 70, 70)), (95, 355, (240, 160, 50)), (120, 355, (230, 200, 60)), (150, 355, (160, 50, 60)), (175, 355, (220, 120, 60))): draw.ellipse((cx - 8, cy - 8, cx + 8, cy + 8), fill=col) draw.rectangle((320, 360, 480, 410), fill=(90, 60, 40), outline=(150, 110, 70), width=2) def _draw_scene_office(draw: ImageDraw.ImageDraw) -> None: wall = (40, 48, 60) floor = (30, 30, 36) draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=wall) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=floor) # Desk draw.rectangle((60, 380, 450, 410), fill=(60, 40, 25), outline=(140, 100, 60), width=2) draw.line((80, 410, 80, FLOOR_Y), fill=(140, 100, 60), width=3) draw.line((430, 410, 430, FLOOR_Y), fill=(140, 100, 60), width=3) # Monitor draw.rectangle((340, 300, 430, 370), outline=(180, 180, 180), width=3, fill=(20, 30, 50)) draw.rectangle((370, 370, 400, 380), fill=(100, 100, 100)) # Wall clock draw.ellipse((60, 60, 120, 120), outline=(200, 200, 200), width=3, fill=(25, 25, 30)) draw.line((90, 90, 90, 70), fill=(200, 200, 200), width=2) draw.line((90, 90, 108, 90), fill=(200, 200, 200), width=2) def _draw_scene_park(draw: ImageDraw.ImageDraw) -> None: sky = (60, 100, 140) grass = (40, 90, 50) draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=sky) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=grass) # Sun draw.ellipse((400, 60, 470, 130), fill=(240, 220, 120)) # Trees for tx in (70, 440): draw.rectangle((tx - 8, 320, tx + 8, FLOOR_Y), fill=(70, 45, 25)) draw.ellipse((tx - 50, 250, tx + 50, 350), fill=(35, 110, 55)) # Bench draw.rectangle((200, 420, 330, 430), fill=(110, 70, 40)) draw.rectangle((200, 430, 210, FLOOR_Y), fill=(110, 70, 40)) draw.rectangle((320, 430, 330, FLOOR_Y), fill=(110, 70, 40)) def _draw_scene_library(draw: ImageDraw.ImageDraw) -> None: wall = (50, 40, 30) floor = (30, 24, 18) draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=wall) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=floor) # Bookshelves (left and right) for shelf_x in (0, 410): draw.rectangle((shelf_x, 60, shelf_x + 100, FLOOR_Y), outline=(120, 80, 40), width=3, fill=(70, 48, 28)) for row_y in range(90, FLOOR_Y - 20, 60): # books bx = shelf_x + 6 while bx < shelf_x + 94: bw = 7 + (bx * 13 % 9) bh = 40 + (bx * 7 % 12) colors = [(150, 50, 60), (60, 100, 140), (90, 140, 70), (180, 140, 60), (120, 60, 130)] col = colors[(bx // 10) % len(colors)] draw.rectangle((bx, row_y - bh, bx + bw, row_y), fill=col, outline=(20, 20, 20)) bx += bw + 2 draw.line((shelf_x + 2, row_y + 4, shelf_x + 98, row_y + 4), fill=(120, 80, 40), width=2) def _draw_scene_kitchen(draw: ImageDraw.ImageDraw) -> None: wall = (210, 195, 160) backsplash = (180, 200, 210) floor = (85, 70, 55) draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=wall) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=floor) # Floor tile pattern for x in range(0, CANVAS, 40): draw.line((x, FLOOR_Y, x, CANVAS), fill=(60, 50, 40), width=1) for y in range(FLOOR_Y + 18, CANVAS, 18): draw.line((0, y, CANVAS, y), fill=(60, 50, 40), width=1) # Backsplash strip between upper cabinets and counter draw.rectangle((20, 260, 490, 360), fill=backsplash) for x in range(20, 490, 24): draw.line((x, 260, x, 360), fill=(150, 170, 180), width=1) for y in (282, 305, 328, 352): draw.line((20, y, 490, y), fill=(150, 170, 180), width=1) # Upper cabinets with doors + handles draw.rectangle((20, 80, 390, 260), outline=(120, 85, 55), width=3, fill=(165, 120, 75)) for x in (145, 270): draw.line((x, 80, x, 260), fill=(120, 85, 55), width=2) for hx in (125, 250, 375): draw.ellipse((hx - 3, 165, hx + 3, 175), fill=(220, 210, 180)) # Window above the counter (right side) draw.rectangle((410, 100, 490, 240), outline=(120, 85, 55), width=3, fill=(130, 180, 210)) draw.line((450, 100, 450, 240), fill=(120, 85, 55), width=2) draw.line((410, 170, 490, 170), fill=(120, 85, 55), width=2) # Little potted plant on window sill draw.rectangle((432, 240, 468, 258), fill=(140, 80, 50)) draw.ellipse((430, 220, 470, 248), fill=(70, 130, 60)) # Counter (thicker, with edge shadow) draw.rectangle((20, 360, 490, 388), fill=(235, 230, 215), outline=(160, 150, 130), width=2) draw.line((20, 385, 490, 385), fill=(170, 160, 140), width=1) # Lower cabinets with doors + handles draw.rectangle((20, 388, 490, FLOOR_Y), outline=(120, 85, 55), width=3, fill=(140, 100, 65)) for x in (145, 270, 395): draw.line((x, 388, x, FLOOR_Y), fill=(120, 85, 55), width=2) for hx in (85, 210, 335, 445): draw.line((hx - 6, 410, hx + 6, 410), fill=(220, 210, 180), width=3) # Stove on the counter draw.rectangle((290, 330, 400, 380), outline=(90, 90, 95), width=2, fill=(45, 45, 50)) # Control knobs for kx in (300, 320, 370, 390): draw.ellipse((kx - 3, 335, kx + 3, 341), fill=(200, 200, 210)) # Burners for bx, by in ((317, 360), (373, 360)): draw.ellipse((bx - 11, by - 9, bx + 11, by + 9), outline=(180, 180, 190), width=2, fill=(25, 25, 30)) draw.ellipse((bx - 6, by - 5, bx + 6, by + 5), outline=(120, 120, 130), width=1) # Pot on the left burner with steam draw.rectangle((304, 310, 332, 332), fill=(70, 70, 80), outline=(180, 180, 190), width=2) draw.line((300, 308, 336, 308), fill=(200, 200, 210), width=3) for sx in (308, 318, 328): draw.line((sx, 300, sx + 3, 290), fill=(220, 220, 230), width=2) draw.line((sx + 3, 290, sx, 280), fill=(220, 220, 230), width=2) # Fridge on the left draw.rectangle((20, 200, 120, FLOOR_Y), outline=(120, 130, 140), width=3, fill=(225, 230, 235)) draw.line((20, 290, 120, 290), fill=(120, 130, 140), width=2) # Fridge handles draw.line((110, 220, 110, 270), fill=(150, 160, 170), width=3) draw.line((110, 310, 110, 380), fill=(150, 160, 170), width=3) # Cutting board + knife on the right counter draw.rectangle((150, 348, 240, 378), fill=(180, 140, 80), outline=(120, 80, 40), width=1) draw.line((170, 362, 220, 362), fill=(220, 220, 220), width=2) draw.line((220, 362, 224, 360), fill=(60, 50, 40), width=3) def _draw_scene_street(draw: ImageDraw.ImageDraw) -> None: sky = (40, 45, 70) road = (35, 35, 40) sidewalk = (90, 90, 95) draw.rectangle((0, 0, CANVAS, 400), fill=sky) draw.rectangle((0, 400, CANVAS, FLOOR_Y), fill=sidewalk) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=road) # Road dashes for x in range(20, CANVAS, 60): draw.rectangle((x, 485, x + 30, 492), fill=(220, 210, 140)) # Buildings in background for bx, bw, bh in ((10, 110, 280), (130, 90, 220), (230, 130, 300), (370, 100, 250), (480, 60, 200)): by = 400 - bh draw.rectangle((bx, by, bx + bw, 400), fill=(55, 55, 75), outline=(90, 90, 110), width=2) # Windows for wy in range(by + 20, 390, 30): for wx in range(bx + 10, bx + bw - 10, 25): lit = (250, 220, 120) if (wx + wy) % 3 == 0 else (30, 35, 55) draw.rectangle((wx, wy, wx + 12, wy + 16), fill=lit) # Lamp post draw.line((70, FLOOR_Y, 70, 270), fill=(180, 180, 180), width=3) draw.ellipse((55, 250, 85, 280), fill=(250, 220, 120), outline=(180, 180, 180), width=2) def _draw_scene_gym(draw: ImageDraw.ImageDraw) -> None: wall = (50, 55, 70) floor = (60, 45, 35) draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=wall) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=floor) # Mirror (back wall) draw.rectangle((80, 80, 440, 280), outline=(180, 200, 210), width=4, fill=(70, 85, 100)) # Dumbbells on floor for cx in (80, 420): draw.rectangle((cx - 30, 440, cx + 30, 455), fill=(50, 50, 55)) draw.ellipse((cx - 45, 430, cx - 20, 465), fill=(30, 30, 35), outline=(150, 150, 160), width=2) draw.ellipse((cx + 20, 430, cx + 45, 465), fill=(30, 30, 35), outline=(150, 150, 160), width=2) # Weight rack draw.rectangle((200, 330, 310, FLOOR_Y), outline=(150, 150, 160), width=3, fill=(40, 40, 50)) for y in (360, 395, 430): draw.line((200, y, 310, y), fill=(150, 150, 160), width=2) def _draw_scene_living_room(draw: ImageDraw.ImageDraw) -> None: wall = (68, 50, 62) floor = (120, 85, 55) draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=wall) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=floor) # Floorboards for y in range(FLOOR_Y + 14, CANVAS, 18): draw.line((0, y, CANVAS, y), fill=(85, 60, 40), width=1) # Wainscoting / baseboard line draw.line((0, FLOOR_Y - 2, CANVAS, FLOOR_Y - 2), fill=(180, 150, 110), width=2) # Framed picture on the wall draw.rectangle((70, 120, 190, 230), outline=(200, 165, 110), width=4, fill=(160, 130, 90)) draw.polygon([(80, 215), (110, 165), (150, 205), (185, 155), (185, 215)], fill=(60, 110, 70)) # Sun in picture draw.ellipse((160, 130, 180, 150), fill=(240, 210, 120)) # TV mounted on the wall draw.rectangle((330, 150, 490, 270), outline=(40, 40, 50), width=6, fill=(15, 20, 35)) # TV screen glow draw.rectangle((340, 160, 480, 260), fill=(30, 50, 90)) draw.polygon([(360, 200), (420, 180), (460, 220), (380, 250)], fill=(80, 120, 180)) # TV stand draw.rectangle((340, 280, 480, 310), fill=(80, 55, 35), outline=(150, 100, 60), width=2) # --- Sofa (back-left) --- sofa_x1, sofa_y1, sofa_x2, sofa_y2 = 40, 340, 270, FLOOR_Y - 4 # Back draw.rectangle((sofa_x1, sofa_y1 - 30, sofa_x2, sofa_y1 + 10), fill=(100, 55, 90), outline=(160, 100, 140), width=2) # Arms draw.rectangle((sofa_x1, sofa_y1 - 10, sofa_x1 + 22, sofa_y2), fill=(110, 65, 100), outline=(160, 100, 140), width=2) draw.rectangle((sofa_x2 - 22, sofa_y1 - 10, sofa_x2, sofa_y2), fill=(110, 65, 100), outline=(160, 100, 140), width=2) # Seat cushions draw.rectangle((sofa_x1 + 22, sofa_y1 + 10, sofa_x2 - 22, sofa_y2), fill=(90, 50, 80), outline=(160, 100, 140), width=2) for x in (118, 196): draw.line((x, sofa_y1 + 10, x, sofa_y2), fill=(160, 100, 140), width=2) # Throw pillows draw.rectangle((60, 342, 102, 376), fill=(220, 180, 100), outline=(150, 110, 50), width=2) draw.rectangle((208, 342, 252, 376), fill=(180, 70, 70), outline=(120, 40, 40), width=2) # Sofa feet for fx in (sofa_x1 + 6, sofa_x2 - 10): draw.rectangle((fx, sofa_y2, fx + 6, sofa_y2 + 8), fill=(60, 40, 25)) # Coffee table draw.rectangle((190, 440, 330, 458), fill=(110, 75, 45), outline=(170, 120, 70), width=2) for lx in (196, 322): draw.line((lx, 458, lx, FLOOR_Y - 6), fill=(90, 60, 35), width=4) # Mug on the coffee table draw.rectangle((220, 430, 240, 442), fill=(230, 230, 230), outline=(160, 160, 160), width=1) draw.line((240, 432, 244, 440), fill=(160, 160, 160), width=2) # --- Rug / carpet under the skeleton --- rug_x1, rug_y1, rug_x2, rug_y2 = 90, 470, 430, 505 # Outer border draw.rectangle((rug_x1, rug_y1, rug_x2, rug_y2), fill=(150, 75, 55), outline=(230, 180, 110), width=3) # Inner border draw.rectangle((rug_x1 + 10, rug_y1 + 5, rug_x2 - 10, rug_y2 - 5), outline=(230, 180, 110), width=2) # Diamond pattern centers cxr = (rug_x1 + rug_x2) // 2 cyr = (rug_y1 + rug_y2) // 2 for dx in (-120, -60, 0, 60, 120): x = cxr + dx draw.polygon([(x, cyr - 8), (x + 10, cyr), (x, cyr + 8), (x - 10, cyr)], outline=(230, 180, 110), width=1, fill=(180, 100, 70)) # Tassels (fringe) on each short edge for tx in range(rug_x1 - 2, rug_x1 + 18, 4): draw.line((tx, rug_y1 + 4, tx, rug_y1 - 8), fill=(230, 190, 120), width=1) for tx in range(rug_x2 - 16, rug_x2 + 4, 4): draw.line((tx, rug_y2 - 4, tx, rug_y2 + 8), fill=(230, 190, 120), width=1) # --- Floor lamp (left corner) --- lamp_cx = 30 draw.ellipse((lamp_cx - 14, FLOOR_Y - 10, lamp_cx + 14, FLOOR_Y + 4), fill=(90, 65, 40), outline=(150, 110, 70), width=2) draw.line((lamp_cx, FLOOR_Y - 8, lamp_cx, 270), fill=(180, 160, 110), width=3) draw.polygon([(lamp_cx - 28, 270), (lamp_cx + 28, 270), (lamp_cx + 22, 230), (lamp_cx - 22, 230)], fill=(240, 210, 140), outline=(180, 140, 60)) # Warm halo for r, col in ((48, (255, 230, 150)), (60, (255, 220, 130))): draw.ellipse((lamp_cx - r, 270 - r // 2, lamp_cx + r, 270 + r // 2), outline=col, width=1) def _draw_scene_beach(draw: ImageDraw.ImageDraw) -> None: sky = (90, 140, 180) sea = (35, 90, 120) sand = (200, 170, 110) draw.rectangle((0, 0, CANVAS, 330), fill=sky) draw.rectangle((0, 330, CANVAS, FLOOR_Y), fill=sea) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=sand) # Sun draw.ellipse((380, 60, 450, 130), fill=(250, 220, 120)) # Gentle wave lines for y, w in ((360, 180), (395, 260), (430, 340)): draw.line((CANVAS // 2 - w // 2, y, CANVAS // 2 + w // 2, y), fill=(180, 210, 225), width=2) # Palm tree (left) draw.line((70, FLOOR_Y, 65, 280), fill=(90, 60, 30), width=6) for dx, dy in ((-40, -10), (-30, -40), (10, -40), (40, -10), (0, 30)): draw.polygon([(65, 280), (65 + dx, 280 + dy - 30), (65 + dx + 6, 280 + dy - 20)], fill=(40, 110, 55)) # Beach umbrella (right) draw.line((440, FLOOR_Y, 440, 360), fill=(80, 80, 80), width=3) draw.pieslice((380, 320, 500, 400), start=180, end=360, fill=(200, 70, 70), outline=(140, 40, 40), width=2) def _draw_scene_forest(draw: ImageDraw.ImageDraw) -> None: sky = (60, 80, 60) ground = (40, 55, 30) draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=sky) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=ground) # Scatter of trees at different depths/sizes trees = [(30, 400, 60, 170), (110, 380, 50, 190), (210, 380, 50, 180), (330, 390, 55, 170), (430, 380, 60, 180), (70, 300, 40, 130), (250, 310, 42, 120), (400, 300, 40, 130)] for tx, trunk_top, canopy_r, trunk_h in trees: trunk_x1 = tx - 6 trunk_x2 = tx + 6 draw.rectangle((trunk_x1, trunk_top, trunk_x2, trunk_top + trunk_h), fill=(70, 45, 25)) draw.ellipse((tx - canopy_r, trunk_top - canopy_r, tx + canopy_r, trunk_top + canopy_r), fill=(30, 90, 45), outline=(20, 60, 30), width=2) # Ground tufts for x in range(0, CANVAS, 40): draw.line((x, FLOOR_Y + 4, x + 8, FLOOR_Y - 2), fill=(60, 90, 40), width=2) def _draw_scene_restaurant(draw: ImageDraw.ImageDraw) -> None: wall = (55, 35, 45) floor = (35, 22, 28) draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=wall) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=floor) # Wine rack / wall decor draw.rectangle((60, 100, 230, 220), outline=(140, 100, 60), width=3, fill=(70, 45, 30)) for y in (130, 170, 210): draw.line((60, y, 230, y), fill=(140, 100, 60), width=2) for x in (90, 120, 150, 180, 210): draw.line((x, 100, x, 220), fill=(140, 100, 60), width=1) # Hanging lamp draw.line((256, 0, 256, 140), fill=(140, 140, 140), width=2) draw.pieslice((220, 130, 292, 180), start=0, end=180, fill=(230, 200, 90), outline=(180, 160, 60), width=2) # Table + tablecloth draw.rectangle((130, 400, 380, 420), fill=(230, 230, 230)) draw.polygon([(130, 420), (380, 420), (360, FLOOR_Y), (150, FLOOR_Y)], fill=(210, 210, 220)) # Wine glass draw.polygon([(240, 360), (270, 360), (265, 395), (245, 395)], fill=(140, 30, 40)) draw.line((255, 395, 255, 410), fill=(200, 200, 200), width=2) draw.line((240, 410, 270, 410), fill=(200, 200, 200), width=2) def _draw_scene_school(draw: ImageDraw.ImageDraw) -> None: wall = (200, 180, 130) floor = (110, 80, 50) draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=wall) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=floor) # Blackboard draw.rectangle((60, 80, 450, 260), fill=(35, 60, 50), outline=(90, 60, 30), width=4) # Chalk text lines for y in range(110, 250, 22): w = 30 + ((y * 7) % 120) draw.line((90, y, 90 + w, y), fill=(220, 220, 220), width=2) # "Equation" draw.line((310, 200, 340, 200), fill=(250, 230, 180), width=3) draw.line((345, 185, 345, 215), fill=(250, 230, 180), width=3) draw.line((355, 200, 385, 200), fill=(250, 230, 180), width=3) draw.line((395, 200, 410, 200), fill=(250, 230, 180), width=3) # Desk draw.rectangle((180, 410, 340, 430), fill=(120, 80, 50)) draw.line((190, 430, 190, FLOOR_Y), fill=(120, 80, 50), width=4) draw.line((330, 430, 330, FLOOR_Y), fill=(120, 80, 50), width=4) def _draw_scene_hospital(draw: ImageDraw.ImageDraw) -> None: wall = (210, 220, 225) floor = (180, 185, 190) draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=wall) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=floor) # Red cross on wall draw.rectangle((50, 80, 130, 180), outline=(160, 160, 160), width=3, fill=(240, 240, 240)) draw.rectangle((82, 90, 98, 170), fill=(200, 40, 40)) draw.rectangle((58, 122, 122, 138), fill=(200, 40, 40)) # Hospital bed draw.rectangle((280, 380, 490, 440), fill=(230, 230, 230), outline=(150, 150, 150), width=2) draw.rectangle((280, 360, 300, 440), fill=(180, 180, 185)) # headboard draw.rectangle((485, 370, 500, 440), fill=(180, 180, 185)) # footboard # Pillow draw.rectangle((300, 370, 360, 395), fill=(255, 255, 255), outline=(180, 180, 180), width=1) # IV stand draw.line((250, FLOOR_Y, 250, 200), fill=(180, 180, 180), width=3) draw.rectangle((238, 200, 262, 240), outline=(160, 160, 160), width=2, fill=(220, 240, 240)) def _draw_scene_bathroom(draw: ImageDraw.ImageDraw) -> None: wall = (200, 215, 220) floor = (220, 215, 205) draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=wall) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=floor) # Tile lines on wall for y in range(80, FLOOR_Y, 40): draw.line((0, y, CANVAS, y), fill=(170, 185, 190), width=1) for x in range(0, CANVAS, 40): draw.line((x, 0, x, FLOOR_Y), fill=(170, 185, 190), width=1) # Mirror draw.rectangle((160, 100, 350, 240), outline=(140, 140, 140), width=4, fill=(150, 170, 180)) # Sink draw.rectangle((160, 370, 350, 400), fill=(245, 245, 245), outline=(160, 160, 160), width=2) draw.ellipse((210, 378, 300, 398), fill=(220, 225, 230), outline=(160, 160, 160), width=2) # Faucet draw.line((255, 370, 255, 355), fill=(190, 190, 200), width=3) draw.line((255, 355, 270, 355), fill=(190, 190, 200), width=3) def _draw_scene_church(draw: ImageDraw.ImageDraw) -> None: wall = (40, 35, 55) floor = (60, 45, 30) draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=wall) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=floor) # Arched stained-glass windows for cx in (120, 256, 392): draw.pieslice((cx - 50, 60, cx + 50, 260), start=180, end=360, fill=(100, 70, 130), outline=(200, 180, 90), width=3) draw.rectangle((cx - 50, 160, cx + 50, 260), fill=(100, 70, 130), outline=(200, 180, 90), width=3) # Cross panes draw.line((cx, 60, cx, 260), fill=(200, 180, 90), width=2) draw.line((cx - 50, 160, cx + 50, 160), fill=(200, 180, 90), width=2) # Altar cross draw.rectangle((248, 340, 264, 440), fill=(200, 180, 90)) draw.rectangle((220, 370, 292, 386), fill=(200, 180, 90)) def _draw_scene_space(draw: ImageDraw.ImageDraw) -> None: draw.rectangle((0, 0, CANVAS, CANVAS), fill=(4, 4, 16)) # Stars star_points = [(37, 52), (91, 120), (163, 40), (220, 95), (278, 60), (340, 110), (410, 45), (470, 100), (60, 220), (130, 270), (250, 310), (345, 240), (450, 280), (25, 350), (185, 400), (400, 380), (75, 450), (300, 450), (490, 460)] for x, y in star_points: draw.ellipse((x - 1, y - 1, x + 2, y + 2), fill=(250, 250, 240)) # Twinkle crosses for x, y in ((140, 150), (390, 180), (210, 370)): draw.line((x - 6, y, x + 6, y), fill=(220, 220, 230), width=1) draw.line((x, y - 6, x, y + 6), fill=(220, 220, 230), width=1) # Planet (bottom-left) draw.ellipse((20, 420, 160, 560), fill=(120, 70, 40), outline=(180, 100, 60), width=2) # Moon crescent (top right) draw.ellipse((390, 80, 460, 150), fill=(220, 220, 230)) draw.ellipse((405, 80, 470, 150), fill=(4, 4, 16)) # Distant nebula glow for r, col in ((90, (60, 30, 80)), (60, (90, 40, 110)), (30, (140, 80, 180))): draw.ellipse((280 - r, 260 - r, 280 + r, 260 + r), outline=col, width=1) def _draw_scene_rooftop(draw: ImageDraw.ImageDraw) -> None: sky = (30, 35, 60) rooftop = (65, 60, 55) draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=sky) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=rooftop) # City skyline silhouettes in distance skyline = [(0, 340, 80, 150), (80, 320, 60, 170), (140, 300, 100, 190), (240, 330, 70, 160), (310, 310, 90, 180), (400, 320, 60, 170), (460, 340, 80, 150)] for bx, by, bw, bh in skyline: by2 = 400 draw.rectangle((bx, by, bx + bw, by2), fill=(45, 50, 85), outline=(70, 80, 120), width=1) for wy in range(by + 20, by2, 18): for wx in range(bx + 8, bx + bw - 8, 14): if (wx + wy) % 3 == 0: draw.rectangle((wx, wy, wx + 6, wy + 8), fill=(240, 220, 130)) # Railing on rooftop draw.line((0, 410, CANVAS, 410), fill=(160, 160, 160), width=3) for x in range(10, CANVAS, 40): draw.line((x, 410, x, FLOOR_Y), fill=(160, 160, 160), width=2) # Stars for x, y in ((60, 60), (200, 40), (380, 70), (470, 120)): draw.ellipse((x - 1, y - 1, x + 2, y + 2), fill=(230, 230, 240)) def _draw_scene_farm(draw: ImageDraw.ImageDraw) -> None: sky = (120, 170, 200) grass = (70, 120, 50) draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=sky) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=grass) # Sun draw.ellipse((60, 80, 130, 150), fill=(250, 220, 130)) # Distant hills draw.pieslice((-60, 340, 240, 500), start=180, end=360, fill=(60, 100, 50)) draw.pieslice((280, 360, 560, 490), start=180, end=360, fill=(80, 120, 55)) # Red barn bx1, by1, bx2, by2 = 300, 320, 470, FLOOR_Y draw.rectangle((bx1, by1 + 30, bx2, by2), fill=(150, 50, 40), outline=(90, 30, 20), width=2) draw.polygon([(bx1, by1 + 30), (bx2, by1 + 30), ((bx1 + bx2) / 2, by1)], fill=(120, 40, 30), outline=(90, 30, 20)) # Barn door draw.rectangle((360, 400, 410, FLOOR_Y), fill=(70, 40, 20), outline=(20, 20, 20), width=2) # X pattern on door draw.line((360, 400, 410, FLOOR_Y), fill=(180, 140, 80), width=2) draw.line((410, 400, 360, FLOOR_Y), fill=(180, 140, 80), width=2) # Fence on the left draw.line((20, 420, 250, 420), fill=(180, 160, 120), width=3) draw.line((20, 440, 250, 440), fill=(180, 160, 120), width=3) for x in range(30, 250, 30): draw.line((x, 410, x, FLOOR_Y), fill=(180, 160, 120), width=3) def _draw_scene_soccer_field(draw: ImageDraw.ImageDraw) -> None: sky = (110, 160, 210) grass = (50, 130, 60) draw.rectangle((0, 0, CANVAS, 280), fill=sky) draw.rectangle((0, 280, CANVAS, CANVAS), fill=grass) # Distant stadium stands (curved silhouette) for bx, bw, bh in ((0, 120, 60), (120, 100, 70), (220, 140, 80), (360, 110, 70), (470, 50, 60)): by = 280 - bh draw.rectangle((bx, by, bx + bw, 280), fill=(60, 70, 90), outline=(40, 45, 60), width=1) # Crowd dots for cx, cy in [(40, 240), (80, 245), (140, 235), (180, 240), (230, 220), (280, 230), (330, 235), (380, 225), (430, 240), (480, 235)]: draw.ellipse((cx - 3, cy - 3, cx + 3, cy + 3), fill=(200, 200, 220)) # Grass stripes for i, y in enumerate(range(280, CANVAS, 30)): shade = (45, 125, 55) if i % 2 else (55, 140, 65) draw.rectangle((0, y, CANVAS, y + 30), fill=shade) # Field lines draw.line((0, 340, CANVAS, 340), fill=(240, 240, 240), width=2) draw.ellipse((180, 320, 332, 360), outline=(240, 240, 240), width=2) # Left goal (back of field) draw.rectangle((30, 310, 130, 352), outline=(240, 240, 240), width=3) draw.line((30, 310, 130, 310), fill=(240, 240, 240), width=3) # Goal net pattern for x in range(40, 130, 14): draw.line((x, 310, x, 352), fill=(200, 200, 200), width=1) for y in range(320, 352, 8): draw.line((30, y, 130, y), fill=(200, 200, 200), width=1) # Penalty arc draw.arc((5, 285, 170, 400), start=-60, end=60, fill=(240, 240, 240), width=2) def _draw_scene_cricket_ground(draw: ImageDraw.ImageDraw) -> None: sky = (120, 170, 210) grass = (60, 140, 65) pitch_color = (190, 165, 115) draw.rectangle((0, 0, CANVAS, 300), fill=sky) draw.rectangle((0, 300, CANVAS, CANVAS), fill=grass) # Oval boundary rope draw.ellipse((-60, 340, CANVAS + 60, 540), outline=(255, 255, 255), width=3) # Pitch (brown rectangle down the middle) pitch_pts = [(200, 370), (312, 370), (350, FLOOR_Y + 20), (162, FLOOR_Y + 20)] draw.polygon(pitch_pts, fill=pitch_color, outline=(150, 120, 80)) # Crease lines for py in (390, 440): draw.line((200 + (py - 370) * 0.3, py, 312 - (py - 370) * 0.3, py), fill=(255, 255, 255), width=2) # Wickets at top and bottom of pitch for wx, wy in ((256, 376), (256, 460)): for dx in (-6, 0, 6): draw.line((wx + dx, wy - 12, wx + dx, wy + 6), fill=(200, 180, 120), width=2) draw.line((wx - 8, wy - 14, wx + 8, wy - 14), fill=(200, 180, 120), width=2) # Distant stadium draw.rectangle((0, 250, CANVAS, 300), fill=(55, 65, 85), outline=(30, 35, 50), width=2) for x in range(10, CANVAS, 16): draw.rectangle((x, 260, x + 8, 290), fill=(80, 90, 110)) def _draw_scene_basketball_court(draw: ImageDraw.ImageDraw) -> None: wall = (50, 55, 70) floor = (190, 135, 75) draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=wall) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=floor) # Wooden floor planks for y in range(FLOOR_Y + 8, CANVAS, 14): draw.line((0, y, CANVAS, y), fill=(150, 100, 55), width=1) # Key (painted free-throw lane) on the floor draw.polygon([(180, FLOOR_Y + 4), (332, FLOOR_Y + 4), (370, CANVAS - 2), (142, CANVAS - 2)], outline=(230, 230, 230), width=3, fill=None) # Free-throw circle draw.arc((195, FLOOR_Y - 4, 317, FLOOR_Y + 38), start=0, end=180, fill=(230, 230, 230), width=3) # Hoop + backboard at the back of the court draw.rectangle((196, 180, 316, 260), fill=(240, 240, 250), outline=(80, 80, 100), width=4) # Backboard "square" draw.rectangle((232, 210, 280, 244), outline=(200, 40, 40), width=3) # Rim draw.ellipse((226, 256, 286, 276), outline=(220, 80, 40), width=4) # Net (triangle of lines hanging) for dx in range(-24, 25, 6): draw.line((256 + dx * 0.6, 266, 256 + dx * 0.3, 302), fill=(220, 220, 220), width=1) # Scoreboard in the upper corner draw.rectangle((40, 70, 190, 130), fill=(20, 25, 40), outline=(180, 180, 200), width=2) draw.line((115, 78, 115, 122), fill=(200, 200, 220), width=1) for tx, text_x in ((70, 90), (140, 160)): for bit_y in (90, 100, 110): draw.line((tx, bit_y, tx + 14, bit_y), fill=(220, 60, 60), width=2) def _draw_scene_tennis_court(draw: ImageDraw.ImageDraw) -> None: sky = (130, 190, 230) court = (150, 85, 65) # clay out = (75, 110, 55) draw.rectangle((0, 0, CANVAS, 300), fill=sky) draw.rectangle((0, 300, CANVAS, CANVAS), fill=out) # Court area (trapezoid due to perspective) draw.polygon([(90, 330), (422, 330), (500, CANVAS), (12, CANVAS)], fill=court, outline=(240, 240, 240)) # Baselines draw.line((12, CANVAS - 2, 500, CANVAS - 2), fill=(240, 240, 240), width=2) draw.line((90, 330, 422, 330), fill=(240, 240, 240), width=2) # Centerline draw.line((256, 330, 256, CANVAS), fill=(240, 240, 240), width=2) # Service line draw.line((130, 390, 382, 390), fill=(240, 240, 240), width=2) # Net across the far baseline draw.line((80, 330, 432, 330), fill=(255, 255, 255), width=4) for x in range(90, 430, 8): draw.line((x, 315, x, 335), fill=(220, 220, 220), width=1) # Net posts draw.line((80, 300, 80, 340), fill=(200, 200, 200), width=3) draw.line((432, 300, 432, 340), fill=(200, 200, 200), width=3) # Umpire chair (right) draw.line((470, 280, 470, 330), fill=(160, 140, 100), width=3) draw.rectangle((460, 260, 484, 282), fill=(110, 80, 50), outline=(160, 120, 70), width=2) def _draw_scene_baseball_field(draw: ImageDraw.ImageDraw) -> None: sky = (120, 170, 220) outfield = (60, 140, 60) infield = (170, 120, 70) draw.rectangle((0, 0, CANVAS, 280), fill=sky) draw.rectangle((0, 280, CANVAS, CANVAS), fill=outfield) # Diamond (infield) — tilted square from batter POV draw.polygon([(256, 310), (460, 420), (256, CANVAS - 4), (52, 420)], fill=infield, outline=(240, 240, 240)) # Bases (white squares) for bx, by in ((256, 310), (460, 420), (256, CANVAS - 4), (52, 420)): draw.rectangle((bx - 10, by - 10, bx + 10, by + 10), fill=(255, 255, 255), outline=(200, 200, 200)) # Pitcher's mound draw.ellipse((236, 390, 276, 420), fill=(200, 155, 100), outline=(140, 100, 60)) # Outfield fence + wall ads draw.line((0, 280, CANVAS, 280), fill=(200, 200, 200), width=3) for x in range(20, CANVAS, 50): draw.rectangle((x, 255, x + 40, 278), fill=(180, 50, 50), outline=(90, 20, 20)) # Bleachers in distance draw.rectangle((0, 200, CANVAS, 250), fill=(60, 70, 90)) for y in (208, 220, 232, 244): draw.line((0, y, CANVAS, y), fill=(100, 110, 130), width=1) def _draw_scene_golf_course(draw: ImageDraw.ImageDraw) -> None: sky = (160, 200, 230) fairway = (110, 180, 80) rough = (70, 130, 60) sand = (220, 200, 140) draw.rectangle((0, 0, CANVAS, 270), fill=sky) draw.rectangle((0, 270, CANVAS, CANVAS), fill=rough) # Distant rolling hills draw.pieslice((-80, 220, 260, 360), start=180, end=360, fill=(80, 150, 70)) draw.pieslice((260, 230, 600, 360), start=180, end=360, fill=(70, 140, 65)) # Sun draw.ellipse((400, 80, 460, 140), fill=(250, 230, 140)) # Fairway (winding shape) draw.polygon([(140, 300), (372, 300), (470, CANVAS), (42, CANVAS)], fill=fairway) # Bunker / sand trap draw.ellipse((90, 400, 210, 450), fill=sand, outline=(180, 150, 90)) # Flagstick + hole flag_x, flag_y = 380, 350 draw.ellipse((flag_x - 8, flag_y - 3, flag_x + 8, flag_y + 5), fill=(40, 40, 50)) draw.line((flag_x, flag_y, flag_x, flag_y - 80), fill=(220, 220, 220), width=3) draw.polygon([(flag_x, flag_y - 80), (flag_x + 35, flag_y - 74), (flag_x, flag_y - 60)], fill=(220, 60, 60)) # Small ball on the tee near front draw.ellipse((250, FLOOR_Y - 4, 262, FLOOR_Y + 8), fill=(255, 255, 255)) draw.line((256, FLOOR_Y + 10, 256, FLOOR_Y + 18), fill=(200, 170, 100), width=2) def _draw_scene_bowling_alley(draw: ImageDraw.ImageDraw) -> None: back = (30, 25, 40) lane = (200, 150, 90) gutter = (50, 45, 60) draw.rectangle((0, 0, CANVAS, CANVAS), fill=back) # Lane (trapezoid receding to a vanishing point) draw.polygon([(120, FLOOR_Y), (392, FLOOR_Y), (310, 200), (202, 200)], fill=lane, outline=(120, 85, 45), width=2) # Gutters on either side of the lane draw.polygon([(80, FLOOR_Y), (120, FLOOR_Y), (202, 200), (190, 200)], fill=gutter) draw.polygon([(432, FLOOR_Y), (392, FLOOR_Y), (310, 200), (322, 200)], fill=gutter) # Lane arrows (aiming dots) for y, sc in ((340, 1.0), (300, 0.8), (260, 0.6)): cx = 256 w = 80 * sc draw.polygon([(cx, y - 6 * sc), (cx + w / 2, y + 4 * sc), (cx, y + 10 * sc), (cx - w / 2, y + 4 * sc)], outline=(240, 220, 160), width=1) # Pin deck at the back draw.rectangle((190, 185, 322, 215), fill=(130, 100, 60), outline=(80, 60, 30), width=2) # 10 pins (triangular formation) at the back rows = [(1, 256), (2, 248), (3, 240), (4, 232)] for row_i, top_y in enumerate(reversed([200, 195, 190, 185])): n = row_i + 1 spacing = 12 start_x = 256 - spacing * (n - 1) / 2 for i in range(n): px = start_x + i * spacing py = top_y # Pin body draw.ellipse((px - 4, py - 12, px + 4, py + 4), fill=(250, 250, 250), outline=(180, 180, 180)) # Red collar draw.line((px - 3, py - 7, px + 3, py - 7), fill=(200, 40, 40), width=2) # Ball return on the right draw.rectangle((430, FLOOR_Y - 40, 495, FLOOR_Y), outline=(100, 100, 120), width=3, fill=(60, 60, 80)) draw.ellipse((445, FLOOR_Y - 32, 483, FLOOR_Y - 4), fill=(140, 40, 140), outline=(90, 20, 90), width=2) draw.ellipse((460, FLOOR_Y - 24, 468, FLOOR_Y - 16), fill=(40, 10, 40)) # --------------------------------------------------------------------------- # Extra scenes. Each uses simple colored shapes and distinctive props so the # location reads at a glance. # --------------------------------------------------------------------------- def _room_bg(draw: ImageDraw.ImageDraw, wall: Tuple[int, int, int], floor: Tuple[int, int, int]) -> None: draw.rectangle((0, 0, CANVAS, FLOOR_Y), fill=wall) draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=floor) def _outdoor_bg(draw: ImageDraw.ImageDraw, sky: Tuple[int, int, int], ground: Tuple[int, int, int], horizon: int = FLOOR_Y) -> None: draw.rectangle((0, 0, CANVAS, horizon), fill=sky) draw.rectangle((0, horizon, CANVAS, CANVAS), fill=ground) def _draw_scene_classroom(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (180, 160, 110), (110, 80, 50)) # Blackboard draw.rectangle((60, 80, 430, 240), fill=(30, 60, 50), outline=(100, 70, 40), width=4) for y in range(110, 230, 20): w = 30 + ((y * 7) % 120) draw.line((90, y, 90 + w, y), fill=(230, 230, 230), width=2) # Teacher desk with apple draw.rectangle((340, 380, 490, FLOOR_Y), fill=(110, 75, 45), outline=(160, 110, 70), width=2) draw.ellipse((430, 365, 450, 385), fill=(200, 50, 50), outline=(80, 20, 20), width=1) draw.line((440, 365, 444, 355), fill=(50, 80, 40), width=2) # Student desks (small rows in background) for x in range(40, 340, 60): draw.rectangle((x, 350, x + 40, 370), fill=(140, 100, 60), outline=(80, 50, 20), width=1) draw.line((x + 8, 370, x + 8, 400), fill=(80, 50, 20), width=2) draw.line((x + 32, 370, x + 32, 400), fill=(80, 50, 20), width=2) def _draw_scene_auditorium(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (40, 25, 50), (60, 30, 40)) # Stage (bright rectangle at back) draw.rectangle((20, 300, 490, 400), fill=(120, 60, 70), outline=(200, 150, 100), width=3) # Curtains on either side of stage for x in (20, 490): dx = -1 if x < 100 else 1 for i in range(6): draw.polygon([(x, 80), (x + dx * 30, 80), (x + dx * 28, 400), (x - dx * 2, 400)], fill=(130, 30, 40), outline=(80, 15, 20)) # Podium center-stage draw.rectangle((236, 330, 276, 390), fill=(160, 120, 70), outline=(90, 60, 30), width=2) # Spotlight cone draw.polygon([(256, 80), (200, 330), (312, 330)], fill=(255, 240, 180)) # Seat rows in the foreground silhouette for y in range(430, CANVAS, 18): draw.rectangle((10, y, CANVAS - 10, y + 10), fill=(30, 20, 40)) def _draw_scene_laboratory(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (200, 215, 225), (170, 180, 190)) # Periodic table on wall draw.rectangle((40, 80, 220, 200), outline=(120, 140, 160), width=3, fill=(240, 245, 250)) for gx in range(48, 218, 12): for gy in range(88, 195, 12): draw.rectangle((gx, gy, gx + 10, gy + 10), outline=(160, 170, 185), width=1) # Bench draw.rectangle((20, 380, 490, 400), fill=(220, 220, 230), outline=(160, 160, 170), width=2) draw.rectangle((20, 400, 490, FLOOR_Y), fill=(160, 160, 170)) # Flasks and beakers for i, (cx, h, col) in enumerate([(280, 30, (80, 200, 120)), (330, 50, (200, 80, 100)), (380, 40, (80, 120, 220))]): # Beaker draw.rectangle((cx - 14, 380 - h, cx + 14, 378), outline=(150, 160, 180), width=2, fill=(230, 240, 250)) draw.rectangle((cx - 12, 380 - h + 6, cx + 12, 378), fill=col) # Bubble draw.ellipse((cx - 2, 380 - h + 10, cx + 2, 380 - h + 14), fill=(230, 240, 250)) # Microscope draw.rectangle((420, 360, 470, 380), fill=(50, 55, 70), outline=(120, 130, 150), width=2) draw.rectangle((435, 310, 455, 360), fill=(50, 55, 70), outline=(120, 130, 150), width=2) draw.ellipse((430, 300, 460, 320), fill=(50, 55, 70), outline=(120, 130, 150), width=2) def _draw_scene_museum(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (220, 210, 195), (120, 100, 80)) # Marble columns for cx in (80, 432): draw.rectangle((cx - 20, 60, cx + 20, FLOOR_Y), fill=(240, 235, 220), outline=(160, 150, 130), width=2) draw.rectangle((cx - 26, 60, cx + 26, 80), fill=(230, 225, 210), outline=(160, 150, 130), width=2) draw.rectangle((cx - 26, FLOOR_Y - 20, cx + 26, FLOOR_Y), fill=(230, 225, 210), outline=(160, 150, 130), width=2) # Framed artifact draw.rectangle((180, 140, 332, 300), outline=(200, 170, 100), width=6, fill=(70, 60, 40)) draw.polygon([(210, 280), (256, 170), (302, 280)], fill=(180, 160, 110)) # Velvet rope for x in (150, 230, 310, 390): draw.rectangle((x - 4, 430, x + 4, 460), fill=(150, 120, 70)) draw.ellipse((x - 8, 420, x + 8, 436), fill=(200, 170, 100), outline=(140, 110, 60)) draw.line((158, 438, 386, 438), fill=(140, 30, 40), width=4) def _draw_scene_art_gallery(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (235, 235, 235), (200, 200, 200)) # Three paintings evenly spaced for i, col in enumerate([(70, 120, 180), (200, 100, 80), (160, 160, 90)]): x1 = 60 + i * 140 draw.rectangle((x1, 140, x1 + 100, 280), outline=(60, 60, 60), width=6, fill=(240, 240, 240)) draw.rectangle((x1 + 10, 150, x1 + 90, 270), fill=col) # Abstract splash draw.ellipse((x1 + 20, 170, x1 + 50, 200), fill=(250, 240, 120)) draw.polygon([(x1 + 50, 220), (x1 + 80, 200), (x1 + 85, 250)], fill=(255, 255, 255)) # Spotlights for i in range(3): lx = 110 + i * 140 draw.line((lx, 80, lx, 120), fill=(180, 180, 180), width=2) draw.polygon([(lx - 18, 120), (lx + 18, 120), (lx + 10, 140), (lx - 10, 140)], fill=(80, 80, 90)) def _draw_scene_theater(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (40, 15, 25), (30, 10, 20)) # Red velvet curtains (sides) for x in (0, CANVAS - 60): for i in range(6): px = x + i * 10 draw.rectangle((px, 60, px + 10, FLOOR_Y - 20), fill=(150, 30, 40), outline=(100, 15, 25), width=1) # Top valance draw.rectangle((0, 60, CANVAS, 110), fill=(180, 40, 50)) for x in range(0, CANVAS, 30): draw.arc((x, 100, x + 30, 130), start=180, end=360, fill=(140, 20, 30), width=3) # Stage draw.rectangle((60, 400, 452, FLOOR_Y), fill=(90, 60, 40), outline=(160, 110, 70), width=2) # Footlights for x in range(80, 445, 30): draw.ellipse((x - 5, 395, x + 5, 405), fill=(250, 220, 130)) def _draw_scene_cinema(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (20, 20, 35), (40, 20, 40)) # Screen draw.rectangle((60, 80, 450, 280), fill=(240, 240, 250), outline=(80, 80, 100), width=6) # Scene on screen (simple shapes) draw.polygon([(80, 260), (180, 160), (280, 260)], fill=(60, 100, 60)) draw.ellipse((330, 110, 400, 180), fill=(250, 230, 140)) # Aisle draw.polygon([(200, 330), (312, 330), (400, CANVAS), (112, CANVAS)], fill=(70, 30, 45)) # Seat rows (back to front) for y in range(340, CANVAS, 22): for x in range(0, CANVAS, 24): draw.rectangle((x + 4, y, x + 18, y + 14), fill=(40, 15, 30), outline=(20, 5, 15), width=1) def _draw_scene_concert_hall(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (80, 50, 40), (50, 30, 25)) # Stage with grand piano draw.rectangle((50, 380, 462, FLOOR_Y), fill=(120, 80, 50), outline=(180, 130, 80), width=2) # Piano (big black shape) draw.polygon([(210, 340), (330, 340), (390, 390), (150, 390)], fill=(20, 20, 25), outline=(120, 120, 140), width=3) draw.rectangle((210, 340, 330, 360), fill=(20, 20, 25), outline=(150, 150, 170)) # Keyboard draw.rectangle((200, 360, 340, 380), fill=(240, 240, 240), outline=(120, 120, 140), width=2) for x in range(200, 340, 8): draw.line((x, 360, x, 380), fill=(60, 60, 70), width=1) # Tiered seating for y in range(420, CANVAS, 20): draw.rectangle((0, y, CANVAS, y + 14), fill=(70, 40, 30)) def _draw_scene_stadium(draw: ImageDraw.ImageDraw) -> None: sky = (120, 170, 210) field = (60, 140, 65) draw.rectangle((0, 0, CANVAS, 320), fill=sky) draw.rectangle((0, 320, CANVAS, CANVAS), fill=field) # Curved stands for y in range(180, 320, 18): draw.arc((-80, y - 40, CANVAS + 80, y + 140), start=0, end=180, fill=(70, 80, 100), width=10) # Crowd dots for cx in range(30, CANVAS, 14): for cy in range(190, 310, 18): if (cx + cy) % 3 == 0: draw.ellipse((cx - 2, cy - 2, cx + 2, cy + 2), fill=(220, 220, 240)) # Track line draw.line((0, 330, CANVAS, 330), fill=(240, 240, 240), width=3) # Goalposts (distant) draw.line((240, 335, 240, 355), fill=(240, 240, 240), width=3) draw.line((272, 335, 272, 355), fill=(240, 240, 240), width=3) draw.line((240, 335, 272, 335), fill=(240, 240, 240), width=3) def _draw_scene_zoo(draw: ImageDraw.ImageDraw) -> None: _outdoor_bg(draw, (140, 180, 210), (90, 140, 70)) # Cage bars for x in range(40, 260, 20): draw.line((x, 120, x, 400), fill=(60, 60, 70), width=3) draw.rectangle((40, 120, 260, 400), outline=(60, 60, 70), width=4) # Animal silhouette (elephant-like) draw.ellipse((90, 300, 220, 390), fill=(120, 110, 120)) draw.ellipse((190, 280, 240, 320), fill=(120, 110, 120)) draw.line((230, 320, 250, 380), fill=(120, 110, 120), width=6) # Legs for lx in (110, 140, 170, 200): draw.rectangle((lx, 370, lx + 10, 400), fill=(100, 90, 100)) # Signpost draw.rectangle((350, 360, 420, 400), fill=(220, 180, 90), outline=(130, 100, 50), width=2) draw.line((385, 400, 385, FLOOR_Y), fill=(130, 100, 50), width=3) def _draw_scene_aquarium(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (25, 30, 50), (15, 20, 35)) # Big water tank draw.rectangle((40, 80, 472, 420), outline=(140, 160, 190), width=6, fill=(40, 100, 150)) # Fish silhouettes for fx, fy, col in ((120, 180, (240, 140, 40)), (280, 220, (220, 220, 140)), (360, 160, (240, 240, 240)), (200, 300, (200, 80, 120)), (400, 340, (140, 200, 240))): draw.ellipse((fx - 18, fy - 8, fx + 18, fy + 8), fill=col) draw.polygon([(fx - 18, fy), (fx - 28, fy - 8), (fx - 28, fy + 8)], fill=col) # Bubbles for bx, by in ((80, 380), (100, 340), (90, 300), (110, 260)): draw.ellipse((bx - 4, by - 4, bx + 4, by + 4), outline=(200, 220, 240), width=1) # Seaweed for x in (80, 200, 340, 440): draw.line((x, 420, x, 360), fill=(40, 130, 70), width=3) def _draw_scene_greenhouse(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (170, 210, 200), (120, 90, 60)) # Glass panel grid on walls for x in range(0, CANVAS, 40): draw.line((x, 0, x, FLOOR_Y), fill=(210, 230, 220), width=1) for y in range(0, FLOOR_Y, 40): draw.line((0, y, CANVAS, y), fill=(210, 230, 220), width=1) # Roof slope draw.polygon([(0, 0), (CANVAS, 0), (CANVAS - 30, 60), (30, 60)], fill=(140, 180, 170)) # Rows of potted plants for i, y in enumerate((390, FLOOR_Y - 8)): for x in range(40, CANVAS, 80): draw.rectangle((x - 14, y, x + 14, y + 18), fill=(140, 70, 40), outline=(80, 40, 20), width=1) draw.ellipse((x - 20, y - 24, x + 20, y + 4), fill=(60, 140, 70), outline=(30, 90, 40), width=1) def _draw_scene_cave(draw: ImageDraw.ImageDraw) -> None: draw.rectangle((0, 0, CANVAS, CANVAS), fill=(20, 15, 25)) # Jagged opening (bright exit) draw.polygon([(60, 80), (120, 60), (200, 100), (256, 70), (310, 110), (380, 70), (450, 100), (460, 220), (380, 200), (320, 250), (220, 230), (160, 260), (90, 240)], fill=(40, 30, 50)) # Stalactites for x in range(40, CANVAS, 60): draw.polygon([(x, 0), (x + 14, 0), (x + 7, 40 + (x * 3) % 30)], fill=(50, 40, 60), outline=(20, 10, 20)) # Stalagmites for x in range(30, CANVAS, 70): draw.polygon([(x, FLOOR_Y), (x + 16, FLOOR_Y), (x + 8, FLOOR_Y - 40 - (x * 5) % 30)], fill=(50, 40, 60), outline=(20, 10, 20)) # Glow draw.ellipse((220, 180, 292, 230), fill=(80, 60, 30)) def _draw_scene_mountain(draw: ImageDraw.ImageDraw) -> None: _outdoor_bg(draw, (150, 200, 230), (90, 130, 70)) # Back peaks draw.polygon([(-40, 320), (140, 120), (320, 300)], fill=(100, 120, 160), outline=(60, 80, 120)) draw.polygon([(200, 320), (380, 80), (560, 320)], fill=(110, 130, 170), outline=(70, 90, 130)) # Snow caps draw.polygon([(110, 180), (140, 120), (170, 180), (150, 190), (130, 190)], fill=(240, 245, 250)) draw.polygon([(350, 140), (380, 80), (410, 140), (390, 155), (370, 155)], fill=(240, 245, 250)) # Pine trees for tx in (40, 90, 450, 480): draw.rectangle((tx - 3, 380, tx + 3, 430), fill=(70, 45, 25)) draw.polygon([(tx - 16, 390), (tx + 16, 390), (tx, 330)], fill=(40, 100, 60)) def _draw_scene_desert(draw: ImageDraw.ImageDraw) -> None: _outdoor_bg(draw, (240, 180, 120), (220, 170, 100), horizon=340) # Sun draw.ellipse((380, 90, 460, 170), fill=(250, 210, 100)) # Rolling dunes for color, y in (((200, 150, 80), 380), ((180, 130, 70), 420)): draw.pieslice((-100, y - 40, 280, y + 80), start=180, end=360, fill=color) draw.pieslice((240, y - 50, 620, y + 60), start=180, end=360, fill=color) # Cactus cx = 100 draw.rectangle((cx - 6, 360, cx + 6, 450), fill=(60, 120, 60), outline=(30, 80, 30), width=2) draw.rectangle((cx - 18, 380, cx - 6, 410), fill=(60, 120, 60), outline=(30, 80, 30), width=2) draw.rectangle((cx + 6, 400, cx + 18, 420), fill=(60, 120, 60), outline=(30, 80, 30), width=2) def _draw_scene_waterfall(draw: ImageDraw.ImageDraw) -> None: _outdoor_bg(draw, (100, 160, 200), (60, 120, 70)) # Cliff face (left) draw.polygon([(0, 80), (220, 80), (220, FLOOR_Y), (0, FLOOR_Y)], fill=(80, 70, 60), outline=(40, 35, 30), width=2) draw.polygon([(280, 80), (CANVAS, 80), (CANVAS, FLOOR_Y), (280, FLOOR_Y)], fill=(80, 70, 60), outline=(40, 35, 30), width=2) # Water cascade draw.rectangle((220, 80, 280, FLOOR_Y), fill=(140, 200, 230)) for y in range(100, FLOOR_Y, 14): draw.line((228, y, 272, y + 6), fill=(200, 230, 245), width=1) # Splash pool draw.ellipse((160, FLOOR_Y - 20, 340, FLOOR_Y + 40), fill=(120, 180, 220)) # Mist for mx in (200, 240, 280): draw.ellipse((mx - 12, FLOOR_Y - 30, mx + 12, FLOOR_Y - 10), outline=(220, 235, 245), width=1) def _draw_scene_vineyard(draw: ImageDraw.ImageDraw) -> None: _outdoor_bg(draw, (180, 210, 210), (110, 150, 80)) # Distant hills draw.pieslice((-100, 280, 200, 380), start=180, end=360, fill=(90, 140, 90)) draw.pieslice((200, 290, 560, 390), start=180, end=360, fill=(80, 130, 85)) # Rows of grapevines (trellises) for i, y in enumerate(range(370, CANVAS, 26)): draw.line((0, y, CANVAS, y + i * 4), fill=(140, 110, 70), width=2) for x in range(10, CANVAS, 40 + i * 4): draw.ellipse((x, y - 6, x + 14, y + 6), fill=(100, 50, 120)) draw.rectangle((x + 5, y + 6, x + 9, y + 12), fill=(80, 60, 40)) def _draw_scene_cemetery(draw: ImageDraw.ImageDraw) -> None: _outdoor_bg(draw, (50, 50, 70), (60, 70, 55)) # Moon draw.ellipse((400, 90, 450, 140), fill=(230, 230, 210)) # Bare tree draw.rectangle((50, 250, 62, 430), fill=(50, 40, 30)) for dx, dy in ((-40, -60), (40, -60), (-60, -90), (60, -100)): draw.line((56, 260, 56 + dx, 260 + dy), fill=(50, 40, 30), width=3) # Tombstones for tx in (180, 260, 340, 430): draw.rectangle((tx - 20, 400, tx + 20, 460), fill=(140, 140, 150), outline=(60, 60, 70), width=2) draw.arc((tx - 20, 380, tx + 20, 420), start=180, end=360, fill=(140, 140, 150), width=20) draw.line((tx - 8, 420, tx + 8, 420), fill=(80, 80, 90), width=1) draw.line((tx - 8, 430, tx + 8, 430), fill=(80, 80, 90), width=1) def _draw_scene_castle(draw: ImageDraw.ImageDraw) -> None: _outdoor_bg(draw, (100, 140, 180), (100, 130, 70)) # Big castle silhouette draw.rectangle((120, 180, 392, 440), fill=(160, 150, 130), outline=(90, 80, 60), width=3) # Battlements (crenels) for x in range(120, 392, 30): draw.rectangle((x, 160, x + 15, 180), fill=(160, 150, 130), outline=(90, 80, 60), width=2) # Towers for tx in (80, 400): draw.rectangle((tx, 150, tx + 50, 440), fill=(170, 160, 140), outline=(90, 80, 60), width=3) draw.polygon([(tx - 6, 150), (tx + 56, 150), (tx + 25, 100)], fill=(140, 40, 50)) # Gate draw.rectangle((220, 340, 292, 440), fill=(60, 40, 25), outline=(100, 70, 40), width=3) draw.arc((220, 310, 292, 370), start=180, end=360, fill=(60, 40, 25), width=20) # Flag draw.line((256, 100, 256, 40), fill=(100, 80, 50), width=3) draw.polygon([(256, 40), (290, 50), (256, 60)], fill=(180, 40, 50)) def _draw_scene_mansion(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (70, 60, 80), (150, 115, 80)) # Grand staircase (two sides converging) for step in range(12): y1 = 380 - step * 8 left_x = 100 - step * 5 right_x = 412 + step * 5 draw.polygon([(left_x, y1), (220, y1 - 10), (220, y1), (left_x + 5, y1 + 8)], fill=(120, 80, 50), outline=(80, 50, 20)) draw.polygon([(292, y1 - 10), (right_x, y1), (right_x - 5, y1 + 8), (292, y1)], fill=(120, 80, 50), outline=(80, 50, 20)) # Chandelier draw.line((256, 0, 256, 80), fill=(140, 140, 140), width=2) draw.ellipse((200, 60, 312, 120), fill=(200, 170, 90), outline=(120, 90, 40), width=3) for angle_deg in range(-80, 81, 20): ang = math.radians(angle_deg - 90) lx = 256 + 60 * math.cos(ang) ly = 90 + 60 * math.sin(ang) draw.ellipse((lx - 3, ly - 3, lx + 3, ly + 3), fill=(255, 240, 160)) # Tall windows on either side for x in (40, 412): draw.rectangle((x, 140, x + 60, 340), outline=(200, 180, 130), width=3, fill=(50, 70, 110)) draw.line((x + 30, 140, x + 30, 340), fill=(200, 180, 130), width=2) draw.line((x, 240, x + 60, 240), fill=(200, 180, 130), width=2) def _draw_scene_cottage(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (180, 150, 100), (90, 55, 30)) # Thatched-ish beam lines on the wall for y in range(80, FLOOR_Y, 30): draw.line((0, y, CANVAS, y), fill=(140, 110, 70), width=1) # Hearth / fireplace draw.rectangle((200, 260, 312, 420), fill=(80, 60, 45), outline=(140, 100, 60), width=3) draw.rectangle((220, 300, 292, 420), fill=(30, 20, 15)) # Fire flames for dx, col in ((-16, (240, 120, 40)), (-4, (250, 160, 60)), (12, (240, 120, 40))): draw.polygon([(256 + dx, 400), (256 + dx - 10, 380), (256 + dx + 6, 360), (256 + dx + 2, 340), (256 + dx + 14, 360), (256 + dx + 8, 380)], fill=col) # Mantel draw.rectangle((180, 250, 332, 270), fill=(120, 80, 50), outline=(80, 50, 20), width=2) # Small window draw.rectangle((50, 180, 130, 270), outline=(140, 110, 70), width=3, fill=(160, 200, 210)) draw.line((90, 180, 90, 270), fill=(140, 110, 70), width=2) def _draw_scene_cabin(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (130, 95, 60), (70, 45, 25)) # Log wall lines for y in range(40, FLOOR_Y, 36): draw.line((0, y, CANVAS, y), fill=(80, 55, 30), width=3) draw.ellipse((-8, y - 10, 8, y + 10), fill=(100, 70, 40)) draw.ellipse((CANVAS - 8, y - 10, CANVAS + 8, y + 10), fill=(100, 70, 40)) # Stone fireplace for gy in range(200, 420, 20): for gx in range(200, 312, 22): draw.ellipse((gx, gy, gx + 20, gy + 18), fill=(140, 130, 120), outline=(80, 70, 65)) # Opening draw.rectangle((220, 320, 292, 420), fill=(30, 20, 15)) # Bearskin rug draw.ellipse((100, 475, 400, 510), fill=(70, 45, 25), outline=(30, 20, 10), width=2) for dx in (-120, 120): draw.ellipse((250 + dx - 20, 470, 250 + dx + 20, 490), fill=(70, 45, 25), outline=(30, 20, 10)) def _draw_scene_lighthouse(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (40, 45, 70), (60, 40, 30)) # Spiral staircase (stacked segments) for i in range(12): y = 100 + i * 30 x = 220 + 30 * math.sin(i * 0.6) draw.rectangle((x, y, x + 70, y + 12), fill=(140, 100, 60), outline=(80, 50, 20), width=1) # Lamp at top draw.ellipse((200, 60, 312, 140), fill=(250, 220, 140), outline=(200, 160, 70), width=3) # Light cone draw.polygon([(256, 100), (60, 20), (60, 180)], fill=(255, 240, 170, 120)) # Small sea window draw.ellipse((40, 360, 120, 440), outline=(200, 210, 220), width=3, fill=(60, 110, 150)) def _draw_scene_temple(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (70, 50, 40), (90, 70, 50)) # Columns for cx in (80, 180, 332, 432): draw.rectangle((cx - 18, 100, cx + 18, FLOOR_Y), fill=(200, 180, 140), outline=(120, 100, 70), width=2) draw.rectangle((cx - 24, 100, cx + 24, 120), fill=(180, 160, 120), outline=(120, 100, 70), width=2) # Roof eaves draw.polygon([(20, 100), (CANVAS - 20, 100), (CANVAS - 60, 60), (60, 60)], fill=(130, 40, 40), outline=(80, 20, 25), width=2) # Idol center draw.ellipse((230, 280, 282, 340), fill=(230, 200, 100), outline=(160, 130, 50), width=3) draw.rectangle((240, 320, 272, 400), fill=(230, 200, 100), outline=(160, 130, 50), width=3) # Incense smoke for sx in (210, 302): draw.rectangle((sx - 6, 380, sx + 6, 400), fill=(100, 70, 40)) for i in range(4): draw.arc((sx - 10, 360 - i * 20, sx + 10, 380 - i * 20), start=0, end=180, fill=(200, 200, 220), width=1) def _draw_scene_monastery(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (150, 135, 110), (100, 85, 65)) # Cloister arches for i, x in enumerate(range(20, CANVAS, 90)): draw.rectangle((x, 180, x + 30, FLOOR_Y), fill=(170, 155, 130), outline=(110, 95, 70), width=2) draw.rectangle((x + 30, 280, x + 80, FLOOR_Y), fill=(110, 90, 60)) draw.pieslice((x + 30, 180, x + 80, 300), start=180, end=360, fill=(110, 90, 60)) # Floor tiles for y in range(FLOOR_Y + 10, CANVAS, 20): draw.line((0, y, CANVAS, y), fill=(70, 55, 40), width=1) # Candles for cx in (80, 430): draw.rectangle((cx - 3, 420, cx + 3, 450), fill=(220, 200, 140)) draw.polygon([(cx - 5, 420), (cx, 405), (cx + 5, 420)], fill=(250, 180, 80)) def _draw_scene_airport(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (200, 215, 225), (100, 100, 110)) # Long terminal windows showing a plane draw.rectangle((40, 100, 472, 300), outline=(130, 140, 155), width=4, fill=(120, 160, 200)) for x in range(40, 472, 80): draw.line((x, 100, x, 300), fill=(130, 140, 155), width=2) # Plane draw.ellipse((140, 200, 340, 240), fill=(240, 240, 250), outline=(100, 100, 120), width=2) draw.polygon([(340, 220), (400, 215), (400, 225)], fill=(240, 240, 250), outline=(100, 100, 120)) draw.polygon([(200, 210), (240, 180), (260, 210)], fill=(240, 240, 250)) # Runway line draw.line((0, FLOOR_Y - 30, CANVAS, FLOOR_Y - 30), fill=(220, 210, 80), width=3) # Departure board draw.rectangle((60, 330, 260, 400), fill=(20, 25, 35), outline=(160, 160, 180), width=2) for y in (345, 365, 385): for x in (70, 130, 180, 230): draw.line((x, y, x + 20, y), fill=(240, 200, 60), width=2) def _draw_scene_train_station(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (50, 60, 75), (80, 80, 90)) # Platform edge draw.rectangle((0, FLOOR_Y - 10, CANVAS, FLOOR_Y), fill=(220, 210, 80)) # Train silhouette draw.rectangle((40, 300, 472, FLOOR_Y - 14), fill=(120, 30, 40), outline=(60, 15, 20), width=3) # Windows for x in range(60, 470, 50): draw.rectangle((x, 330, x + 30, 370), fill=(160, 200, 230), outline=(40, 10, 20), width=2) # Wheels for x in (100, 200, 300, 400): draw.ellipse((x - 15, FLOOR_Y - 28, x + 15, FLOOR_Y + 2), fill=(40, 40, 50), outline=(200, 200, 210), width=2) # Overhead sign draw.rectangle((180, 80, 332, 140), fill=(220, 210, 80), outline=(130, 110, 50), width=3) draw.line((200, 110, 310, 110), fill=(40, 40, 40), width=4) def _draw_scene_subway(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (220, 225, 230), (40, 40, 50)) # Tile grid for y in range(60, FLOOR_Y, 24): draw.line((0, y, CANVAS, y), fill=(180, 185, 195), width=1) for x in range(0, CANVAS, 30): draw.line((x, 60, x, FLOOR_Y), fill=(180, 185, 195), width=1) # Arriving train draw.rectangle((120, 280, 500, 440), fill=(80, 90, 120), outline=(40, 50, 70), width=3) for x in range(140, 480, 40): draw.rectangle((x, 310, x + 28, 350), fill=(180, 210, 240), outline=(40, 50, 70), width=2) # Tracks draw.rectangle((0, FLOOR_Y, CANVAS, CANVAS), fill=(60, 50, 40)) for y in range(FLOOR_Y + 8, CANVAS, 16): draw.line((0, y, CANVAS, y), fill=(120, 100, 80), width=2) draw.line((120, FLOOR_Y + 20, CANVAS, FLOOR_Y + 20), fill=(200, 200, 210), width=3) draw.line((120, FLOOR_Y + 40, CANVAS, FLOOR_Y + 40), fill=(200, 200, 210), width=3) def _draw_scene_bridge(draw: ImageDraw.ImageDraw) -> None: _outdoor_bg(draw, (130, 170, 210), (60, 110, 150), horizon=380) # River ripples for y in range(400, CANVAS, 20): for x in range(0, CANVAS, 40): draw.arc((x, y, x + 40, y + 10), start=0, end=180, fill=(140, 180, 210), width=1) # Suspension towers for tx in (90, 422): draw.rectangle((tx - 8, 80, tx + 8, 380), fill=(180, 60, 40), outline=(90, 25, 20), width=2) draw.rectangle((tx - 14, 140, tx + 14, 160), fill=(180, 60, 40)) # Main cables draw.arc((60, 120, 462, 360), start=180, end=360, fill=(40, 40, 50), width=4) # Vertical suspenders for x in range(100, 420, 25): cy = 350 - (abs(x - 256) * 0.7) draw.line((x, cy, x, 380), fill=(60, 60, 70), width=1) # Deck draw.rectangle((0, 380, CANVAS, FLOOR_Y), fill=(100, 100, 110), outline=(60, 60, 70), width=2) def _draw_scene_parking_lot(draw: ImageDraw.ImageDraw) -> None: _outdoor_bg(draw, (180, 180, 200), (80, 80, 90)) # Parking lines for x in range(20, CANVAS, 56): draw.line((x, 320, x, FLOOR_Y), fill=(230, 230, 80), width=2) # Cars (silhouettes) for i, (cx, col) in enumerate([(80, (200, 60, 60)), (200, (60, 100, 180)), (320, (240, 220, 90)), (440, (100, 180, 100))]): draw.rectangle((cx - 25, 360, cx + 25, 410), fill=col, outline=(30, 30, 40), width=2) draw.polygon([(cx - 18, 360), (cx + 18, 360), (cx + 10, 330), (cx - 10, 330)], fill=col, outline=(30, 30, 40)) draw.ellipse((cx - 22, 402, cx - 10, 414), fill=(40, 40, 50)) draw.ellipse((cx + 10, 402, cx + 22, 414), fill=(40, 40, 50)) # Streetlamp draw.line((470, FLOOR_Y, 470, 200), fill=(80, 80, 90), width=3) draw.ellipse((460, 190, 480, 210), fill=(250, 220, 130)) def _draw_scene_gas_station(draw: ImageDraw.ImageDraw) -> None: _outdoor_bg(draw, (140, 180, 210), (90, 90, 95)) # Canopy draw.rectangle((50, 180, 462, 230), fill=(200, 60, 50), outline=(120, 30, 25), width=3) draw.rectangle((40, 230, 60, 440), fill=(140, 140, 150)) draw.rectangle((452, 230, 472, 440), fill=(140, 140, 150)) # Pumps for px in (180, 332): draw.rectangle((px - 15, 340, px + 15, 440), fill=(230, 230, 230), outline=(100, 100, 110), width=2) # Screen draw.rectangle((px - 10, 350, px + 10, 380), fill=(60, 80, 60)) # Nozzle hose draw.line((px + 15, 370, px + 35, 380), fill=(60, 60, 70), width=3) # Convenience store draw.rectangle((300, 260, 490, 440), fill=(230, 215, 180), outline=(140, 110, 70), width=3) draw.rectangle((340, 320, 420, 400), fill=(80, 110, 150), outline=(60, 70, 90), width=2) def _draw_scene_bank(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (220, 200, 160), (160, 130, 100)) # Teller counter draw.rectangle((60, 340, 452, 400), fill=(140, 100, 60), outline=(80, 50, 20), width=3) draw.rectangle((60, 400, 452, FLOOR_Y), fill=(110, 80, 50)) # Teller windows for x in range(80, 440, 90): draw.rectangle((x, 280, x + 60, 340), outline=(80, 50, 20), width=3, fill=(200, 220, 230)) # Safe door draw.ellipse((350, 80, 470, 200), fill=(100, 110, 130), outline=(40, 40, 60), width=4) draw.ellipse((380, 110, 440, 170), fill=(60, 70, 90), outline=(40, 40, 60), width=2) for angle_deg in range(0, 360, 45): ang = math.radians(angle_deg) x = 410 + 22 * math.cos(ang) y = 140 + 22 * math.sin(ang) draw.ellipse((x - 2, y - 2, x + 2, y + 2), fill=(180, 180, 190)) def _draw_scene_prison(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (90, 85, 75), (60, 55, 50)) # Block pattern wall for y in range(80, FLOOR_Y, 30): offset = 15 if (y // 30) % 2 else 0 for x in range(-20 + offset, CANVAS, 60): draw.rectangle((x, y, x + 58, y + 28), outline=(50, 45, 40), width=1, fill=(100, 90, 80)) # Barred cell on right draw.rectangle((280, 200, 480, FLOOR_Y), fill=(40, 35, 30), outline=(20, 15, 10), width=3) for bx in range(290, 480, 18): draw.rectangle((bx - 2, 200, bx + 2, FLOOR_Y), fill=(160, 160, 170)) for by in (250, 340, 400): draw.line((280, by, 480, by), fill=(160, 160, 170), width=3) # Bunk draw.rectangle((310, 360, 440, 400), fill=(120, 100, 90), outline=(60, 50, 40), width=2) def _draw_scene_police_station(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (180, 190, 200), (120, 120, 130)) # Wanted posters for i, x in enumerate((60, 160, 260)): draw.rectangle((x, 100, x + 70, 200), fill=(230, 220, 200), outline=(120, 100, 70), width=2) draw.ellipse((x + 15, 115, x + 55, 155), fill=(100, 80, 70)) draw.line((x + 5, 170, x + 65, 170), fill=(60, 40, 30), width=2) # Desk draw.rectangle((60, 380, 320, 410), fill=(130, 90, 55), outline=(80, 50, 20), width=2) draw.line((80, 410, 80, FLOOR_Y), fill=(80, 50, 20), width=3) draw.line((300, 410, 300, FLOOR_Y), fill=(80, 50, 20), width=3) # Jail-cell corner (right) draw.rectangle((370, 180, 490, FLOOR_Y), outline=(60, 60, 70), width=4, fill=(40, 40, 50)) for bx in range(378, 490, 14): draw.line((bx, 180, bx, FLOOR_Y), fill=(170, 170, 180), width=3) def _draw_scene_fire_station(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (180, 50, 50), (80, 80, 90)) # Red garage door draw.rectangle((80, 120, 430, FLOOR_Y), fill=(180, 40, 40), outline=(100, 20, 20), width=4) for y in range(150, FLOOR_Y, 30): draw.line((80, y, 430, y), fill=(100, 20, 20), width=2) # Fire truck outline through the door draw.rectangle((120, 340, 390, 440), fill=(150, 30, 30), outline=(90, 15, 15), width=2) draw.rectangle((130, 290, 220, 340), fill=(150, 30, 30), outline=(90, 15, 15), width=2) draw.ellipse((130, 420, 170, 460), fill=(30, 30, 40)) draw.ellipse((330, 420, 370, 460), fill=(30, 30, 40)) # Fire-pole (right side) draw.line((470, 80, 470, FLOOR_Y), fill=(220, 200, 90), width=6) def _draw_scene_courthouse(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (120, 100, 80), (80, 60, 40)) # Columns (4) for cx in (60, 180, 300, 420): draw.rectangle((cx, 80, cx + 30, FLOOR_Y), fill=(220, 210, 190), outline=(130, 120, 100), width=2) draw.rectangle((cx - 4, 80, cx + 34, 100), fill=(200, 190, 170), outline=(130, 120, 100), width=2) # Judge's bench draw.rectangle((120, 360, 392, 430), fill=(90, 60, 35), outline=(150, 100, 55), width=3) # Gavel on desk draw.rectangle((220, 340, 250, 360), fill=(130, 90, 50), outline=(70, 45, 20), width=2) draw.line((240, 346, 280, 346), fill=(130, 90, 50), width=5) # Scales of justice draw.line((256, 260, 256, 340), fill=(180, 150, 70), width=3) draw.line((210, 260, 302, 260), fill=(180, 150, 70), width=3) for px in (210, 302): draw.arc((px - 15, 260, px + 15, 280), start=0, end=180, fill=(180, 150, 70), width=3) def _draw_scene_factory(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (90, 95, 110), (60, 65, 75)) # Smokestack (tall, left) draw.rectangle((30, 100, 90, FLOOR_Y), fill=(150, 80, 60), outline=(80, 40, 30), width=3) # Smoke for sx, sy in ((40, 80), (60, 60), (50, 40)): draw.ellipse((sx - 15, sy - 15, sx + 15, sy + 15), fill=(120, 120, 130)) # Conveyor belt draw.rectangle((120, 360, 480, 400), fill=(50, 55, 65), outline=(130, 130, 150), width=3) for x in range(130, 470, 30): draw.ellipse((x, 365, x + 20, 395), outline=(160, 160, 180), width=2) # Boxes on belt for bx in (160, 260, 360): draw.rectangle((bx - 15, 340, bx + 15, 360), fill=(180, 140, 90), outline=(100, 70, 40), width=2) draw.line((bx - 15, 350, bx + 15, 350), fill=(100, 70, 40), width=1) # Machinery gears in bg for cx, cy in ((200, 200), (280, 160), (360, 210)): r = 28 draw.ellipse((cx - r, cy - r, cx + r, cy + r), fill=(110, 100, 90), outline=(60, 50, 40), width=3) for angle_deg in range(0, 360, 45): ang = math.radians(angle_deg) x = cx + r * math.cos(ang) y = cy + r * math.sin(ang) draw.rectangle((x - 4, y - 4, x + 4, y + 4), fill=(80, 70, 60)) def _draw_scene_warehouse(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (140, 130, 110), (70, 65, 55)) # Tall shelving racks for rx in (40, 180, 320, 440): draw.rectangle((rx, 100, rx + 60, FLOOR_Y), fill=(80, 75, 70), outline=(30, 25, 20), width=2) for y in (160, 240, 320, 400): draw.line((rx, y, rx + 60, y), fill=(30, 25, 20), width=2) # Boxes on shelves for i in range(3): bx = rx + 5 + i * 18 draw.rectangle((bx, y - 30, bx + 14, y - 2), fill=(180, 140, 90), outline=(100, 70, 40), width=1) # Forklift draw.rectangle((240, 400, 310, 460), fill=(220, 180, 50), outline=(130, 100, 30), width=3) draw.line((310, 400, 340, 360), fill=(80, 80, 90), width=4) draw.line((310, 440, 340, 400), fill=(80, 80, 90), width=4) draw.ellipse((240, 450, 270, 480), fill=(30, 30, 40)) draw.ellipse((285, 450, 315, 480), fill=(30, 30, 40)) def _draw_scene_construction_site(draw: ImageDraw.ImageDraw) -> None: _outdoor_bg(draw, (180, 170, 140), (150, 130, 100)) # Scaffolding for x in (60, 110, 160, 210): draw.line((x, 120, x, FLOOR_Y), fill=(80, 80, 90), width=3) for y in range(140, FLOOR_Y, 40): draw.line((60, y, 210, y), fill=(80, 80, 90), width=3) # Building frame draw.rectangle((270, 160, 470, FLOOR_Y), outline=(140, 80, 40), width=3, fill=(160, 130, 100)) for y in range(200, FLOOR_Y, 60): draw.line((270, y, 470, y), fill=(140, 80, 40), width=2) for x in range(310, 470, 40): draw.line((x, 160, x, FLOOR_Y), fill=(140, 80, 40), width=1) # Cement mixer draw.ellipse((60, 400, 140, 460), fill=(180, 160, 120), outline=(90, 70, 40), width=3) draw.line((100, 400, 100, 370), fill=(120, 100, 70), width=3) # Orange safety cones for cx in (220, 250): draw.polygon([(cx - 8, FLOOR_Y - 4), (cx + 8, FLOOR_Y - 4), (cx, FLOOR_Y - 30)], fill=(230, 110, 40)) draw.line((cx - 6, FLOOR_Y - 18, cx + 6, FLOOR_Y - 18), fill=(240, 240, 240), width=1) def _draw_scene_garage(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (160, 165, 170), (90, 90, 95)) # Pegboard with tools (left) draw.rectangle((30, 100, 220, 260), fill=(220, 180, 100), outline=(130, 100, 50), width=3) for y in range(115, 260, 18): for x in range(40, 220, 18): draw.ellipse((x - 1, y - 1, x + 1, y + 1), fill=(100, 70, 30)) # Hanging tools (hammer, wrench, saw) draw.rectangle((70, 130, 76, 180), fill=(100, 60, 30)) draw.rectangle((60, 120, 90, 140), fill=(70, 70, 80)) draw.rectangle((140, 140, 145, 200), fill=(150, 150, 160)) draw.polygon([(140, 140), (160, 140), (158, 150), (142, 150)], fill=(80, 80, 90)) # Workbench draw.rectangle((30, 320, 260, 360), fill=(110, 75, 45), outline=(60, 40, 20), width=2) draw.line((40, 360, 40, FLOOR_Y), fill=(60, 40, 20), width=4) draw.line((250, 360, 250, FLOOR_Y), fill=(60, 40, 20), width=4) # Toolbox on workbench draw.rectangle((70, 290, 150, 320), fill=(200, 40, 40), outline=(130, 20, 20), width=2) # Half-open garage door (right) draw.rectangle((300, 80, 500, 200), outline=(100, 100, 110), width=4, fill=(60, 80, 100)) for y in range(95, 200, 15): draw.line((300, y, 500, y), fill=(100, 100, 110), width=1) def _draw_scene_basement(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (90, 85, 80), (60, 55, 50)) # Exposed beams for x in range(80, CANVAS, 100): draw.rectangle((x, 60, x + 20, 120), fill=(100, 70, 40), outline=(60, 40, 20), width=2) # Furnace (metal rectangle) draw.rectangle((60, 260, 200, 440), fill=(70, 75, 85), outline=(40, 40, 50), width=3) draw.rectangle((90, 290, 170, 370), fill=(240, 140, 40)) for y in range(300, 370, 8): draw.line((90, y, 170, y), fill=(150, 80, 20), width=1) # Pipes draw.line((200, 300, CANVAS, 300), fill=(160, 110, 40), width=8) draw.line((200, 340, CANVAS, 340), fill=(160, 110, 40), width=8) # Boxes stacked for i, (x, w, h, col) in enumerate([(280, 80, 60, (180, 140, 90)), (290, 60, 40, (160, 120, 80)), (370, 70, 70, (170, 130, 85)), (380, 50, 30, (150, 110, 75))]): y2 = 440 y1 = y2 - h draw.rectangle((x, y1, x + w, y2), fill=col, outline=(80, 50, 20), width=2) draw.line((x, y1 + h // 2, x + w, y1 + h // 2), fill=(80, 50, 20), width=1) def _draw_scene_attic(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (130, 95, 70), (90, 60, 40)) # Sloped rafters (triangular roof shape) draw.polygon([(0, 200), (256, 40), (CANVAS, 200)], fill=(150, 115, 85), outline=(80, 50, 25), width=3) draw.polygon([(0, 200), (0, 60), (80, 60)], fill=(130, 95, 70)) draw.polygon([(CANVAS, 200), (CANVAS, 60), (432, 60)], fill=(130, 95, 70)) for ry in range(80, 200, 30): draw.line((0, ry, CANVAS, ry), fill=(80, 50, 25), width=1) # Circular gable window draw.ellipse((206, 60, 306, 160), fill=(200, 220, 240), outline=(80, 50, 25), width=3) draw.line((256, 60, 256, 160), fill=(80, 50, 25), width=2) draw.line((206, 110, 306, 110), fill=(80, 50, 25), width=2) # Dusty trunk draw.rectangle((140, 380, 260, 440), fill=(110, 70, 40), outline=(60, 35, 15), width=3) draw.arc((140, 360, 260, 400), start=180, end=360, fill=(110, 70, 40), width=15) # Old lamp draw.rectangle((320, 400, 330, 440), fill=(80, 60, 30)) draw.polygon([(306, 400), (344, 400), (340, 380), (310, 380)], fill=(180, 150, 80)) def _draw_scene_laundry_room(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (230, 235, 240), (170, 170, 180)) # Washer draw.rectangle((60, 260, 220, 440), fill=(240, 240, 245), outline=(120, 130, 140), width=3) draw.ellipse((85, 290, 195, 400), outline=(120, 130, 140), width=4, fill=(150, 180, 210)) draw.ellipse((110, 315, 170, 375), outline=(120, 130, 140), width=2, fill=(120, 160, 200)) # Dryer draw.rectangle((240, 260, 400, 440), fill=(240, 240, 245), outline=(120, 130, 140), width=3) draw.ellipse((265, 290, 375, 400), outline=(120, 130, 140), width=4, fill=(60, 70, 90)) # Controls on top for x in (100, 140, 180, 280, 320, 360): draw.ellipse((x - 5, 270, x + 5, 280), outline=(120, 130, 140), width=1, fill=(200, 210, 220)) # Folded clothes stack for i, col in enumerate([(120, 170, 210), (200, 160, 100), (200, 80, 100), (140, 200, 140)]): y = 430 - i * 14 draw.rectangle((430, y, 490, y + 12), fill=col, outline=(60, 60, 70), width=1) def _draw_scene_pantry(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (180, 150, 100), (110, 80, 50)) # Shelves for shelf_y in (90, 170, 250, 330, 410): draw.rectangle((30, shelf_y, 482, shelf_y + 12), fill=(150, 110, 70), outline=(80, 50, 20), width=2) # Jars and cans for shelf_y in (90, 170, 250, 330): for i, x in enumerate(range(50, 480, 38)): col_options = [(200, 170, 110), (180, 60, 60), (100, 150, 90), (230, 200, 90), (140, 100, 150), (200, 130, 80)] col = col_options[(i + shelf_y) % len(col_options)] shape = (x + i * 0, shelf_y - 32, x + 28, shelf_y - 2) if i % 2: draw.rectangle(shape, fill=col, outline=(60, 40, 30), width=1) draw.rectangle((shape[0], shape[1], shape[2], shape[1] + 6), fill=(180, 180, 190)) else: draw.ellipse(shape, fill=col, outline=(60, 40, 30), width=1) # Lid draw.rectangle((shape[0] + 4, shape[1] - 4, shape[2] - 4, shape[1] + 4), fill=(140, 110, 70)) def _draw_scene_swimming_pool(draw: ImageDraw.ImageDraw) -> None: _outdoor_bg(draw, (150, 200, 230), (200, 210, 220), horizon=280) # Pool draw.rectangle((30, 300, 482, FLOOR_Y), fill=(60, 160, 200), outline=(40, 110, 150), width=4) # Lane ropes for y in range(330, FLOOR_Y, 25): for x in range(40, 480, 14): col = (230, 60, 60) if (x // 28) % 2 else (230, 230, 230) draw.ellipse((x, y - 3, x + 10, y + 3), fill=col) # Ripples for y in range(310, FLOOR_Y, 20): for x in range(60, 470, 40): draw.arc((x, y, x + 20, y + 6), start=0, end=180, fill=(100, 180, 220), width=1) # Diving board (right) draw.rectangle((380, 260, 490, 280), fill=(200, 200, 210), outline=(100, 100, 110), width=2) draw.rectangle((470, 280, 486, 310), fill=(80, 80, 90)) def _draw_scene_casino(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (60, 20, 20), (30, 10, 10)) # Neon lights (top band) for x in range(0, CANVAS, 50): col = [(250, 80, 100), (250, 200, 80), (100, 200, 250), (200, 80, 250)][(x // 50) % 4] draw.rectangle((x, 60, x + 40, 80), fill=col) # Roulette table (green) draw.ellipse((60, 300, 452, 460), fill=(30, 90, 60), outline=(200, 170, 80), width=5) # Wheel draw.ellipse((180, 320, 332, 440), outline=(200, 170, 80), width=4, fill=(120, 80, 40)) for angle_deg in range(0, 360, 30): ang = math.radians(angle_deg) x1 = 256 + 60 * math.cos(ang) y1 = 380 + 40 * math.sin(ang) col = (200, 40, 40) if (angle_deg // 30) % 2 else (40, 40, 40) draw.ellipse((x1 - 4, y1 - 4, x1 + 4, y1 + 4), fill=col) # Slot machines on the wall (left/right) for sx in (40, 440): draw.rectangle((sx - 25, 140, sx + 25, 280), fill=(220, 40, 40), outline=(100, 20, 20), width=3) draw.rectangle((sx - 18, 170, sx + 18, 220), fill=(30, 30, 40), outline=(200, 180, 100), width=2) for y in (190, 210): for x in (sx - 10, sx, sx + 10): draw.ellipse((x - 4, y - 4, x + 4, y + 4), fill=(230, 200, 100)) def _draw_scene_nightclub(draw: ImageDraw.ImageDraw) -> None: draw.rectangle((0, 0, CANVAS, CANVAS), fill=(20, 10, 30)) # Disco ball draw.ellipse((216, 70, 296, 150), fill=(180, 190, 210), outline=(100, 100, 120), width=2) for angle_deg in range(0, 360, 22): ang = math.radians(angle_deg) x = 256 + 38 * math.cos(ang) y = 110 + 38 * math.sin(ang) draw.ellipse((x - 3, y - 3, x + 3, y + 3), fill=(240, 240, 255)) # Cord draw.line((256, 0, 256, 70), fill=(80, 80, 90), width=2) # Light beams for x_target, col in ((60, (200, 60, 180)), (460, (60, 200, 220)), (140, (240, 220, 80)), (380, (240, 80, 80))): draw.polygon([(256, 110), (x_target - 20, FLOOR_Y), (x_target + 20, FLOOR_Y)], fill=col + (60,) if len(col) == 3 else col) # DJ booth (back) draw.rectangle((180, 340, 332, 410), fill=(40, 30, 60), outline=(180, 80, 200), width=3) # Turntables for tx in (216, 296): draw.ellipse((tx - 16, 350, tx + 16, 382), outline=(200, 200, 220), width=2, fill=(20, 10, 30)) # Dance floor (checkered glow) for i, y in enumerate(range(420, CANVAS, 20)): for j, x in enumerate(range(0, CANVAS, 40)): col = (200, 80, 220) if (i + j) % 2 else (60, 180, 220) draw.rectangle((x, y, x + 40, y + 20), fill=col) def _draw_scene_arcade(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (30, 25, 50), (40, 30, 60)) # Row of arcade cabinets for i, col in enumerate([(220, 60, 60), (60, 200, 220), (240, 200, 60), (160, 80, 220)]): x = 40 + i * 110 draw.rectangle((x, 120, x + 90, 440), fill=col, outline=(30, 20, 10), width=3) # Screen draw.rectangle((x + 10, 150, x + 80, 230), fill=(30, 30, 50), outline=(220, 220, 240), width=2) # Glowing content on screen draw.rectangle((x + 20, 190, x + 30, 200), fill=(240, 240, 100)) draw.ellipse((x + 40, 170, x + 60, 190), fill=(240, 80, 120)) # Control panel draw.rectangle((x + 10, 260, x + 80, 300), fill=(50, 40, 30), outline=(20, 10, 5), width=2) draw.ellipse((x + 20, 270, x + 32, 288), fill=(200, 200, 210)) draw.ellipse((x + 50, 270, x + 62, 288), fill=(240, 80, 80)) # Coin slot draw.rectangle((x + 38, 330, x + 52, 340), fill=(20, 20, 30)) def _draw_scene_spa(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (220, 230, 220), (200, 200, 180)) # Bamboo wall decor for x in (60, 80, 100, 420, 440, 460): draw.rectangle((x, 120, x + 12, 320), fill=(150, 180, 90), outline=(80, 110, 40), width=2) for y in (150, 190, 230, 270): draw.line((x, y, x + 12, y), fill=(80, 110, 40), width=1) # Massage bed draw.rectangle((120, 380, 400, 410), fill=(230, 220, 210), outline=(150, 140, 120), width=2) draw.rectangle((120, 410, 400, 440), fill=(180, 160, 140)) draw.line((150, 410, 150, FLOOR_Y), fill=(100, 90, 80), width=3) draw.line((370, 410, 370, FLOOR_Y), fill=(100, 90, 80), width=3) # Hole for face draw.ellipse((140, 380, 170, 410), fill=(180, 160, 140)) # Candles for cx in (60, 460): draw.rectangle((cx - 6, 410, cx + 6, 440), fill=(220, 200, 170)) draw.polygon([(cx - 4, 410), (cx, 395), (cx + 4, 410)], fill=(250, 180, 80)) # Stone bowls for bx in (190, 330): draw.ellipse((bx - 20, 355, bx + 20, 380), fill=(160, 150, 130), outline=(80, 70, 60), width=2) draw.ellipse((bx - 10, 358, bx + 10, 370), fill=(90, 130, 180)) def _draw_scene_barn(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (120, 40, 30), (100, 80, 50)) # Wooden plank walls for x in range(0, CANVAS, 30): draw.line((x, 60, x, FLOOR_Y), fill=(60, 20, 15), width=2) # Hayloft (top) draw.rectangle((100, 60, 412, 220), fill=(100, 25, 20), outline=(40, 10, 10), width=3) for y in range(80, 220, 20): draw.line((100, y, 412, y), fill=(40, 10, 10), width=1) # Hay bales (yellow rectangles) for i, x in enumerate((130, 200, 280, 350)): draw.rectangle((x, 130, x + 50, 210), fill=(230, 190, 80), outline=(130, 100, 40), width=2) for hy in (140, 155, 170, 185, 200): draw.line((x, hy, x + 50, hy), fill=(180, 140, 60), width=1) # Cow stall draw.rectangle((40, 300, 180, 440), fill=(60, 40, 30), outline=(30, 20, 15), width=3) draw.line((40, 340, 180, 340), fill=(30, 20, 15), width=2) # Pitchfork leaning draw.line((400, FLOOR_Y, 420, 280), fill=(130, 90, 50), width=4) for dx in (-10, 0, 10): draw.line((420, 280, 420 + dx, 250), fill=(160, 160, 170), width=3) def _draw_scene_salon(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (230, 200, 210), (180, 160, 170)) # Big mirror on wall draw.rectangle((60, 100, 300, 360), outline=(200, 170, 100), width=6, fill=(180, 200, 210)) # Reflected light streaks draw.line((80, 160, 280, 180), fill=(240, 245, 250), width=2) draw.line((80, 220, 280, 240), fill=(240, 245, 250), width=2) # Barber chair draw.rectangle((160, 380, 250, 440), fill=(120, 30, 50), outline=(70, 15, 30), width=3) draw.rectangle((150, 360, 260, 390), fill=(140, 40, 60), outline=(70, 15, 30), width=3) draw.line((205, 440, 205, FLOOR_Y), fill=(120, 120, 130), width=5) draw.ellipse((170, FLOOR_Y - 10, 240, FLOOR_Y + 20), fill=(100, 100, 110), outline=(60, 60, 70), width=2) # Tray with scissors & comb draw.rectangle((340, 380, 470, 420), fill=(120, 120, 130), outline=(60, 60, 70), width=2) draw.line((360, 400, 400, 380), fill=(200, 200, 210), width=2) draw.line((400, 380, 420, 400), fill=(200, 200, 210), width=2) draw.rectangle((420, 385, 460, 395), fill=(40, 120, 200)) def _draw_scene_bakery(draw: ImageDraw.ImageDraw) -> None: _room_bg(draw, (240, 220, 190), (160, 120, 80)) # Chalkboard menu draw.rectangle((40, 80, 200, 220), fill=(40, 60, 50), outline=(160, 110, 60), width=4) for y in range(100, 215, 20): w = 40 + ((y * 5) % 90) draw.line((60, y, 60 + w, y), fill=(230, 230, 220), width=2) # Display case draw.rectangle((240, 300, 490, 420), fill=(240, 230, 200), outline=(140, 100, 60), width=3) draw.rectangle((240, 300, 490, 320), fill=(200, 160, 100)) # Pastries in the case for i, (col, shape) in enumerate([((200, 150, 80), 'circle'), ((220, 100, 80), 'square'), ((240, 200, 120), 'circle'), ((180, 120, 70), 'square'), ((230, 180, 100), 'circle')]): x = 260 + i * 44 if shape == 'circle': draw.ellipse((x, 340, x + 30, 370), fill=col, outline=(120, 80, 40), width=2) draw.ellipse((x + 8, 345, x + 16, 353), fill=(140, 100, 60)) else: draw.rectangle((x, 340, x + 30, 370), fill=col, outline=(120, 80, 40), width=2) draw.line((x, 355, x + 30, 355), fill=(120, 80, 40), width=1) # Flour dust for fx, fy in ((120, 400), (170, 420), (80, 440)): draw.ellipse((fx, fy, fx + 6, fy + 4), fill=(250, 245, 230)) SCENES: Dict[str, Callable[[ImageDraw.ImageDraw], None]] = { "bedroom": _draw_scene_bedroom, "market": _draw_scene_market, "office": _draw_scene_office, "park": _draw_scene_park, "library": _draw_scene_library, "kitchen": _draw_scene_kitchen, "street": _draw_scene_street, "gym": _draw_scene_gym, "beach": _draw_scene_beach, "forest": _draw_scene_forest, "restaurant": _draw_scene_restaurant, "school": _draw_scene_school, "hospital": _draw_scene_hospital, "bathroom": _draw_scene_bathroom, "church": _draw_scene_church, "space": _draw_scene_space, "rooftop": _draw_scene_rooftop, "farm": _draw_scene_farm, "living_room": _draw_scene_living_room, "soccer_field": _draw_scene_soccer_field, "cricket_ground": _draw_scene_cricket_ground, "basketball_court": _draw_scene_basketball_court, "tennis_court": _draw_scene_tennis_court, "baseball_field": _draw_scene_baseball_field, "golf_course": _draw_scene_golf_course, "bowling_alley": _draw_scene_bowling_alley, # Education / institutional "classroom": _draw_scene_classroom, "auditorium": _draw_scene_auditorium, "laboratory": _draw_scene_laboratory, # Culture / entertainment "museum": _draw_scene_museum, "art_gallery": _draw_scene_art_gallery, "theater": _draw_scene_theater, "cinema": _draw_scene_cinema, "concert_hall": _draw_scene_concert_hall, "stadium": _draw_scene_stadium, "zoo": _draw_scene_zoo, "aquarium": _draw_scene_aquarium, # Nature / outdoor "greenhouse": _draw_scene_greenhouse, "cave": _draw_scene_cave, "mountain": _draw_scene_mountain, "desert": _draw_scene_desert, "waterfall": _draw_scene_waterfall, "vineyard": _draw_scene_vineyard, # Architectural / historic "cemetery": _draw_scene_cemetery, "castle": _draw_scene_castle, "mansion": _draw_scene_mansion, "cottage": _draw_scene_cottage, "cabin": _draw_scene_cabin, "lighthouse": _draw_scene_lighthouse, "temple": _draw_scene_temple, "monastery": _draw_scene_monastery, # Transport hubs "airport": _draw_scene_airport, "train_station": _draw_scene_train_station, "subway": _draw_scene_subway, "bridge": _draw_scene_bridge, "parking_lot": _draw_scene_parking_lot, "gas_station": _draw_scene_gas_station, # Civic "bank": _draw_scene_bank, "prison": _draw_scene_prison, "police_station": _draw_scene_police_station, "fire_station": _draw_scene_fire_station, "courthouse": _draw_scene_courthouse, # Industrial "factory": _draw_scene_factory, "warehouse": _draw_scene_warehouse, "construction_site": _draw_scene_construction_site, "garage": _draw_scene_garage, # Home annexes "basement": _draw_scene_basement, "attic": _draw_scene_attic, "laundry_room": _draw_scene_laundry_room, "pantry": _draw_scene_pantry, # Fun / recreation "swimming_pool": _draw_scene_swimming_pool, "casino": _draw_scene_casino, "nightclub": _draw_scene_nightclub, "arcade": _draw_scene_arcade, "spa": _draw_scene_spa, # Misc "barn": _draw_scene_barn, "salon": _draw_scene_salon, "bakery": _draw_scene_bakery, } def _draw_skull(draw: ImageDraw.ImageDraw, head_xy: Tuple[float, float], emotion: str) -> None: cx, cy = head_xy r = 28 # Skull outline draw.ellipse((cx - r, cy - r, cx + r, cy + r), outline=BONE_COLOR, width=3, fill=BG_COLOR) # Jaw hint (two small lines) draw.line((cx - 10, cy + r - 2, cx - 6, cy + r + 6), fill=BONE_COLOR, width=2) draw.line((cx + 10, cy + r - 2, cx + 6, cy + r + 6), fill=BONE_COLOR, width=2) # Eyes (hollow skeleton sockets) eye_y = cy - 4 eye_dx = 9 eye_r = 5 if emotion == "tired": draw.line((cx - eye_dx - 4, eye_y, cx - eye_dx + 4, eye_y), fill=BONE_COLOR, width=3) draw.line((cx + eye_dx - 4, eye_y, cx + eye_dx + 4, eye_y), fill=BONE_COLOR, width=3) elif emotion == "angry": # Sockets + angled brow lines draw.ellipse((cx - eye_dx - eye_r, eye_y - eye_r, cx - eye_dx + eye_r, eye_y + eye_r), fill=BONE_COLOR) draw.ellipse((cx + eye_dx - eye_r, eye_y - eye_r, cx + eye_dx + eye_r, eye_y + eye_r), fill=BONE_COLOR) draw.line((cx - eye_dx - 8, eye_y - 10, cx - eye_dx + 6, eye_y - 4), fill=BONE_COLOR, width=3) draw.line((cx + eye_dx + 8, eye_y - 10, cx + eye_dx - 6, eye_y - 4), fill=BONE_COLOR, width=3) elif emotion == "excited": draw.ellipse((cx - eye_dx - eye_r - 1, eye_y - eye_r - 1, cx - eye_dx + eye_r + 1, eye_y + eye_r + 1), fill=BONE_COLOR) draw.ellipse((cx + eye_dx - eye_r - 1, eye_y - eye_r - 1, cx + eye_dx + eye_r + 1, eye_y + eye_r + 1), fill=BONE_COLOR) elif emotion == "scared" or emotion == "surprised": # Wide open eyes — big empty sockets with a pupil dot big = eye_r + 2 for dx in (-eye_dx, eye_dx): draw.ellipse((cx + dx - big, eye_y - big, cx + dx + big, eye_y + big), outline=BONE_COLOR, width=2, fill=BG_COLOR) draw.ellipse((cx + dx - 2, eye_y - 2, cx + dx + 2, eye_y + 2), fill=BONE_COLOR) elif emotion == "bored": # Droopy half-closed: horizontal ovals for dx in (-eye_dx, eye_dx): draw.ellipse((cx + dx - eye_r - 1, eye_y - 2, cx + dx + eye_r + 1, eye_y + 3), fill=BONE_COLOR) elif emotion == "confused": # Asymmetric: one normal, one slightly squinted + a "?" mark near head draw.ellipse((cx - eye_dx - eye_r, eye_y - eye_r, cx - eye_dx + eye_r, eye_y + eye_r), fill=BONE_COLOR) draw.ellipse((cx + eye_dx - eye_r + 1, eye_y - 2, cx + eye_dx + eye_r - 1, eye_y + 3), fill=BONE_COLOR) # Question mark to the upper-right of the skull qx, qy = cx + r + 14, cy - r + 4 draw.arc((qx - 6, qy - 6, qx + 6, qy + 6), start=200, end=20, fill=BONE_COLOR, width=2) draw.line((qx + 2, qy + 4, qx + 2, qy + 9), fill=BONE_COLOR, width=2) draw.ellipse((qx + 1, qy + 12, qx + 4, qy + 15), fill=BONE_COLOR) else: draw.ellipse((cx - eye_dx - eye_r, eye_y - eye_r, cx - eye_dx + eye_r, eye_y + eye_r), fill=BONE_COLOR) draw.ellipse((cx + eye_dx - eye_r, eye_y - eye_r, cx + eye_dx + eye_r, eye_y + eye_r), fill=BONE_COLOR) # Nasal cavity (triangle) nose_y = cy + 4 draw.polygon([(cx, nose_y), (cx - 3, nose_y + 8), (cx + 3, nose_y + 8)], outline=BONE_COLOR) # Mouth (teeth grid) — emotion-shaped my = cy + 14 mw = 18 mh = 6 if emotion == "happy": draw.arc((cx - mw, my - mh, cx + mw, my + mh + 4), start=0, end=180, fill=BONE_COLOR, width=3) # Teeth ticks for i in range(-2, 3): draw.line((cx + i * 5, my - 2, cx + i * 5, my + 2), fill=BONE_COLOR, width=1) elif emotion == "sad": draw.arc((cx - mw, my + 2, cx + mw, my + mh + 10), start=180, end=360, fill=BONE_COLOR, width=3) elif emotion == "angry": # Zigzag teeth pts = [] for i in range(7): pts.append((cx - mw + i * (mw * 2 // 6), my + (mh if i % 2 else -mh // 2))) draw.line(pts, fill=BONE_COLOR, width=2) elif emotion == "excited": draw.ellipse((cx - mw // 2, my - 4, cx + mw // 2, my + 10), outline=BONE_COLOR, width=3, fill=BG_COLOR) elif emotion == "tired": draw.line((cx - mw // 2, my + 4, cx + mw // 2, my + 4), fill=BONE_COLOR, width=2) elif emotion == "scared": # Wavy trembling line pts = [(cx - mw + i * (mw * 2 // 8), my + (3 if i % 2 else -3)) for i in range(9)] draw.line(pts, fill=BONE_COLOR, width=2) elif emotion == "surprised": # Small round 'O' draw.ellipse((cx - mw // 3, my - 4, cx + mw // 3, my + 10), outline=BONE_COLOR, width=3, fill=BG_COLOR) elif emotion == "bored": # Flat line slanted down-right draw.line((cx - mw // 2, my + 2, cx + mw // 2, my + 6), fill=BONE_COLOR, width=2) elif emotion == "confused": # Squiggly mouth pts = [(cx - mw // 2 + i * (mw // 6), my + (2 if i % 2 else 6)) for i in range(7)] draw.line(pts, fill=BONE_COLOR, width=2) else: # neutral draw.line((cx - mw // 2, my + 2, cx + mw // 2, my + 2), fill=BONE_COLOR, width=2) for i in range(-1, 2): draw.line((cx + i * 5, my - 1, cx + i * 5, my + 5), fill=BONE_COLOR, width=1) def _draw_ribs(draw: ImageDraw.ImageDraw, neck: Tuple[float, float], pelvis: Tuple[float, float]) -> None: # Spine goes from neck to pelvis; ribs are short arcs on each side. nx, ny = neck px, py = pelvis for i in range(1, 5): frac = i / 5.0 cy = ny + (py - ny) * frac w = 24 - i * 2 # taper draw.arc((nx - w, cy - 8, nx + w, cy + 8), start=200, end=340, fill=BONE_COLOR, width=2) def _draw_pelvis(draw: ImageDraw.ImageDraw, pelvis: Tuple[float, float]) -> None: cx, cy = pelvis draw.arc((cx - 26, cy - 4, cx + 26, cy + 26), start=0, end=180, fill=BONE_COLOR, width=3) def _draw_book(draw: ImageDraw.ImageDraw, lh: Tuple[float, float], rh: Tuple[float, float]) -> None: # Open book held between the two hands: a slight V-shape with two pages # visible and ruled lines suggesting text. cx = (lh[0] + rh[0]) / 2 cy = (lh[1] + rh[1]) / 2 - 6 half_w = max(30, abs(rh[0] - lh[0]) / 2 + 12) page_h = 34 # Cover (red) — rounded-ish trapezoid behind the pages for depth cover_pts = [ (cx - half_w - 4, cy + page_h // 2 + 4), (cx + half_w + 4, cy + page_h // 2 + 4), (cx + half_w + 2, cy - page_h // 2 - 2), (cx - half_w - 2, cy - page_h // 2 - 2), ] draw.polygon(cover_pts, fill=(150, 40, 50), outline=(90, 20, 30)) # Two pages, each tilted slightly outward (the V of an open book) left_page = [ (cx, cy - page_h // 2), (cx - half_w, cy - page_h // 2 + 4), (cx - half_w + 2, cy + page_h // 2), (cx, cy + page_h // 2 - 2), ] right_page = [ (cx, cy - page_h // 2), (cx + half_w, cy - page_h // 2 + 4), (cx + half_w - 2, cy + page_h // 2), (cx, cy + page_h // 2 - 2), ] draw.polygon(left_page, fill=(245, 235, 210), outline=(160, 140, 100)) draw.polygon(right_page, fill=(245, 235, 210), outline=(160, 140, 100)) # Ruled text lines on each page for i in range(1, 5): ly = cy - page_h // 2 + i * 7 draw.line((cx - half_w + 8, ly, cx - 4, ly), fill=(120, 100, 70), width=1) draw.line((cx + 4, ly, cx + half_w - 8, ly), fill=(120, 100, 70), width=1) # Spine crease draw.line((cx, cy - page_h // 2, cx, cy + page_h // 2 - 2), fill=(120, 90, 60), width=2) def _draw_vacuum(draw: ImageDraw.ImageDraw, lh: Tuple[float, float], rh: Tuple[float, float]) -> None: # Handle from midpoint of hands down to a "vacuum head" on the floor. gx = (lh[0] + rh[0]) / 2 gy = (lh[1] + rh[1]) / 2 hx = gx + 10 hy = FLOOR_Y - 6 draw.line((gx, gy, hx, hy - 30), fill=(180, 180, 190), width=4) draw.line((hx, hy - 30, hx, hy), fill=(200, 200, 210), width=5) # Vacuum head (rectangle) draw.rectangle((hx - 38, hy - 12, hx + 38, hy + 6), fill=(220, 80, 90), outline=(120, 30, 40), width=2) # Bristle strip on bottom for bx in range(int(hx - 34), int(hx + 34), 8): draw.line((bx, hy + 6, bx, hy + 12), fill=(210, 210, 210), width=1) def _draw_broom(draw: ImageDraw.ImageDraw, lh: Tuple[float, float], rh: Tuple[float, float]) -> None: # A long handle from the higher hand down to bristles near the floor. top = lh if lh[1] < rh[1] else rh bot = rh if lh[1] < rh[1] else lh head_x = bot[0] + 18 head_y = FLOOR_Y - 8 draw.line((top[0], top[1], head_x, head_y - 20), fill=(160, 110, 60), width=5) # Bristle bundle draw.polygon([(head_x - 22, head_y - 20), (head_x + 22, head_y - 20), (head_x + 30, head_y + 14), (head_x - 30, head_y + 14)], fill=(220, 190, 110), outline=(160, 120, 60), width=2) # Bristle lines for i in range(-5, 6): draw.line((head_x + i * 4, head_y - 18, head_x + i * 5, head_y + 12), fill=(180, 140, 80), width=1) def _draw_pot(draw: ImageDraw.ImageDraw, lh: Tuple[float, float], rh: Tuple[float, float]) -> None: cx = (lh[0] + rh[0]) / 2 cy = max(lh[1], rh[1]) + 12 draw.rectangle((cx - 32, cy - 14, cx + 32, cy + 18), fill=(60, 60, 70), outline=(180, 180, 200), width=2) # Lid / rim draw.line((cx - 36, cy - 14, cx + 36, cy - 14), fill=(200, 200, 220), width=3) # Steam (3 wavy lines above the pot) for dx in (-12, 0, 12): x = cx + dx draw.line((x, cy - 28, x + 4, cy - 40), fill=(200, 210, 220), width=2) draw.line((x + 4, cy - 40, x, cy - 52), fill=(200, 210, 220), width=2) def _draw_sink(draw: ImageDraw.ImageDraw, lh: Tuple[float, float], rh: Tuple[float, float]) -> None: cx = (lh[0] + rh[0]) / 2 cy = max(lh[1], rh[1]) + 8 # Basin draw.rectangle((cx - 60, cy - 4, cx + 60, cy + 36), fill=(220, 225, 230), outline=(160, 160, 170), width=2) # Water draw.rectangle((cx - 56, cy + 8, cx + 56, cy + 32), fill=(100, 160, 200)) # Bubbles for bx, by, br in ((cx - 30, cy + 12, 4), (cx - 10, cy + 18, 3), (cx + 20, cy + 14, 5), (cx + 40, cy + 22, 3)): draw.ellipse((bx - br, by - br, bx + br, by + br), outline=(230, 240, 250), width=1) def _draw_spray_and_cloth(draw: ImageDraw.ImageDraw, lh: Tuple[float, float], rh: Tuple[float, float]) -> None: """Spray bottle in left hand, wiping cloth in right hand.""" # Spray bottle (L hand) lx, ly = lh draw.rectangle((lx - 8, ly - 22, lx + 8, ly + 4), fill=(60, 180, 200), outline=(20, 100, 120), width=2) # Nozzle draw.rectangle((lx + 6, ly - 20, lx + 16, ly - 12), fill=(30, 120, 140), outline=(20, 80, 100), width=1) # Trigger draw.line((lx + 8, ly - 10, lx + 8, ly + 4), fill=(20, 100, 120), width=2) # Mist (three small drops) for i, dx in enumerate((18, 24, 30)): draw.ellipse((lx + dx, ly - 22 + i * 3, lx + dx + 4, ly - 18 + i * 3), fill=(200, 230, 240)) # Cloth (R hand) rx, ry = rh draw.polygon([(rx - 18, ry - 2), (rx + 18, ry - 4), (rx + 20, ry + 10), (rx - 16, ry + 12)], fill=(230, 210, 120), outline=(160, 130, 70), width=2) # Fold line draw.line((rx - 4, ry + 2, rx + 6, ry + 4), fill=(160, 130, 70), width=1) def _draw_feather_duster(draw: ImageDraw.ImageDraw, hand: Tuple[float, float]) -> None: """Feather duster sticking up/out from the raised hand.""" hx, hy = hand # Handle draw.line((hx, hy, hx + 6, hy - 18), fill=(150, 100, 60), width=3) # Feather bundle (fan shape) fx, fy = hx + 6, hy - 18 for angle_deg in (-40, -20, 0, 20, 40): angle = math.radians(angle_deg - 90) ex = fx + 28 * math.cos(angle) ey = fy + 28 * math.sin(angle) draw.line((fx, fy, ex, ey), fill=(230, 180, 120), width=4) # Tufts at the end draw.ellipse((ex - 4, ey - 4, ex + 4, ey + 4), fill=(250, 220, 160)) def _draw_mop(draw: ImageDraw.ImageDraw, lh: Tuple[float, float], rh: Tuple[float, float]) -> None: """A mop: long handle down to a stringy head on the floor.""" top = lh if lh[1] < rh[1] else rh bot = rh if lh[1] < rh[1] else lh head_x = bot[0] + 16 head_y = FLOOR_Y - 6 draw.line((top[0], top[1], head_x, head_y - 30), fill=(130, 180, 190), width=4) # Mop head (rounded cluster) draw.ellipse((head_x - 30, head_y - 32, head_x + 30, head_y + 4), fill=(230, 225, 205), outline=(180, 175, 150), width=2) # Strings dangling for dx in range(-22, 23, 5): draw.line((head_x + dx, head_y - 6, head_x + dx + 2, head_y + 14), fill=(220, 210, 180), width=1) # Water splashes for sx in (head_x - 38, head_x + 36): draw.ellipse((sx - 4, FLOOR_Y + 2, sx + 4, FLOOR_Y + 8), outline=(130, 180, 210), width=1) def _draw_iron_and_board(draw: ImageDraw.ImageDraw, lh: Tuple[float, float], rh: Tuple[float, float]) -> None: """Ironing board stretching across the body + the iron in the right hand.""" # Board board_y = max(lh[1], rh[1]) + 22 draw.polygon([(150, board_y), (380, board_y), (360, board_y + 12), (170, board_y + 12)], fill=(220, 215, 200), outline=(160, 150, 130), width=2) # Board legs draw.line((180, board_y + 12, 160, board_y + 70), fill=(120, 120, 130), width=3) draw.line((350, board_y + 12, 370, board_y + 70), fill=(120, 120, 130), width=3) # Shirt being ironed (rectangle on the board) draw.rectangle((195, board_y - 10, 345, board_y), fill=(180, 200, 230), outline=(120, 140, 170), width=1) # Iron (R hand) rx, ry = rh draw.polygon([(rx - 20, ry - 6), (rx + 18, ry - 10), (rx + 24, ry + 4), (rx - 18, ry + 8)], fill=(60, 60, 70), outline=(180, 180, 200), width=2) # Iron handle draw.rectangle((rx - 10, ry - 18, rx + 10, ry - 6), fill=(40, 40, 50), outline=(160, 160, 180), width=2) # Steam for sx in (rx - 8, rx, rx + 8): draw.line((sx, ry - 20, sx + 2, ry - 30), fill=(210, 220, 230), width=2) def _draw_polish_cloth(draw: ImageDraw.ImageDraw, hand: Tuple[float, float]) -> None: """Polishing cloth in the hand + a sparkle on the polished surface.""" hx, hy = hand # Cloth draw.polygon([(hx - 14, hy - 4), (hx + 14, hy - 2), (hx + 16, hy + 10), (hx - 12, hy + 12)], fill=(240, 230, 180), outline=(180, 160, 110), width=2) # Sparkles on a nearby surface for sx, sy in ((hx - 40, hy + 20), (hx + 40, hy + 30), (hx + 10, hy + 50)): draw.line((sx - 4, sy, sx + 4, sy), fill=(255, 240, 180), width=1) draw.line((sx, sy - 4, sx, sy + 4), fill=(255, 240, 180), width=1) draw.ellipse((sx - 1, sy - 1, sx + 2, sy + 2), fill=(255, 255, 220)) def _draw_soccer_ball(draw: ImageDraw.ImageDraw, foot: Tuple[float, float]) -> None: """Soccer ball near a foot.""" fx, fy = foot bx = fx + 18 by = fy + 4 r = 14 draw.ellipse((bx - r, by - r, bx + r, by + r), fill=(250, 250, 250), outline=(40, 40, 50), width=2) # Pentagon pattern (5 black spots) for dx, dy in ((0, -6), (-7, -1), (7, -1), (-4, 5), (4, 5)): draw.polygon([(bx + dx, by + dy - 3), (bx + dx + 3, by + dy), (bx + dx + 1, by + dy + 3), (bx + dx - 2, by + dy + 2), (bx + dx - 3, by + dy - 1)], fill=(30, 30, 40)) def _draw_cricket_bat(draw: ImageDraw.ImageDraw, lh: Tuple[float, float], rh: Tuple[float, float]) -> None: """Cricket bat held by both hands — blade extending down past the hands.""" grip_x = (lh[0] + rh[0]) / 2 grip_y = (lh[1] + rh[1]) / 2 # Handle (thin, above the grip) draw.line((grip_x, grip_y - 30, grip_x, grip_y + 4), fill=(160, 100, 60), width=4) # Blade blade_top_y = grip_y + 4 blade_bot_y = min(FLOOR_Y - 8, blade_top_y + 90) draw.rectangle((grip_x - 12, blade_top_y, grip_x + 12, blade_bot_y), fill=(230, 215, 170), outline=(150, 120, 70), width=2) # Maker sticker draw.rectangle((grip_x - 6, blade_top_y + 20, grip_x + 6, blade_top_y + 36), fill=(200, 40, 50)) # Wickets behind the batsman wx = grip_x + 70 wy_top = blade_bot_y - 30 for dx in (-6, 0, 6): draw.line((wx + dx, wy_top, wx + dx, blade_bot_y), fill=(220, 200, 150), width=2) draw.line((wx - 8, wy_top - 2, wx + 8, wy_top - 2), fill=(220, 200, 150), width=2) def _draw_basketball(draw: ImageDraw.ImageDraw, hand: Tuple[float, float]) -> None: """Orange basketball below the dribbling hand.""" bx, by = hand r = 16 draw.ellipse((bx - r, by - r, bx + r, by + r), fill=(220, 110, 40), outline=(140, 60, 20), width=2) # Seam lines draw.arc((bx - r, by - r, bx + r, by + r), start=15, end=165, fill=(120, 50, 20), width=2) draw.line((bx - r, by, bx + r, by), fill=(120, 50, 20), width=2) draw.line((bx, by - r, bx, by + r), fill=(120, 50, 20), width=2) def _draw_tennis_racket(draw: ImageDraw.ImageDraw, hand: Tuple[float, float]) -> None: """Tennis racket extending out from the hand.""" hx, hy = hand # Handle draw.line((hx, hy, hx + 20, hy - 24), fill=(40, 40, 50), width=4) # Head (oval) head_cx, head_cy = hx + 36, hy - 44 draw.ellipse((head_cx - 22, head_cy - 28, head_cx + 22, head_cy + 28), outline=(200, 200, 210), width=4, fill=None) # Strings for dx in (-14, -7, 0, 7, 14): draw.line((head_cx + dx, head_cy - 24, head_cx + dx, head_cy + 24), fill=(220, 220, 220), width=1) for dy in (-18, -9, 0, 9, 18): draw.line((head_cx - 20, head_cy + dy, head_cx + 20, head_cy + dy), fill=(220, 220, 220), width=1) def _draw_baseball_bat(draw: ImageDraw.ImageDraw, lh: Tuple[float, float], rh: Tuple[float, float]) -> None: """Baseball bat held with both hands, angled over the shoulder.""" grip_x = (lh[0] + rh[0]) / 2 grip_y = (lh[1] + rh[1]) / 2 # Bat extends up-right over the shoulder end_x = grip_x + 70 end_y = grip_y - 100 # Taper: draw as a thick line for the barrel + thinner for handle draw.line((grip_x, grip_y, end_x, end_y), fill=(160, 110, 60), width=10) # Knob at the grip end draw.ellipse((grip_x - 6, grip_y - 6, grip_x + 6, grip_y + 6), fill=(120, 80, 40), outline=(60, 40, 20), width=2) # Barrel end (thicker) draw.ellipse((end_x - 8, end_y - 8, end_x + 8, end_y + 8), fill=(200, 150, 90), outline=(120, 80, 40), width=2) def _draw_golf_club(draw: ImageDraw.ImageDraw, lh: Tuple[float, float], rh: Tuple[float, float]) -> None: """Golf club and tiny ball on tee.""" grip_x = (lh[0] + rh[0]) / 2 grip_y = (lh[1] + rh[1]) / 2 # Shaft head_x = grip_x - 40 head_y = FLOOR_Y - 8 draw.line((grip_x, grip_y, head_x, head_y), fill=(200, 200, 210), width=3) # Club head (wedge) draw.polygon([(head_x - 14, head_y - 4), (head_x + 6, head_y - 8), (head_x + 10, head_y + 4), (head_x - 10, head_y + 6)], fill=(60, 60, 70), outline=(20, 20, 30), width=2) # Ball on tee ball_x, ball_y = head_x - 40, FLOOR_Y - 6 draw.ellipse((ball_x - 5, ball_y - 5, ball_x + 5, ball_y + 5), fill=(255, 255, 255), outline=(180, 180, 180), width=1) draw.line((ball_x, ball_y + 5, ball_x, ball_y + 12), fill=(200, 170, 100), width=2) def _draw_bowling_ball(draw: ImageDraw.ImageDraw, hand: Tuple[float, float]) -> None: """Large bowling ball held by the swinging hand.""" bx, by = hand r = 18 draw.ellipse((bx - r, by - r, bx + r, by + r), fill=(80, 30, 120), outline=(40, 10, 60), width=2) # Finger holes for dx, dy in ((-5, -4), (2, -5), (-1, 3)): draw.ellipse((bx + dx - 2, by + dy - 2, bx + dx + 2, by + dy + 2), fill=(30, 10, 50)) def _draw_skateboard(draw: ImageDraw.ImageDraw, lf: Tuple[float, float], rf: Tuple[float, float]) -> None: """Skateboard deck + wheels under the feet.""" x1 = min(lf[0], rf[0]) - 20 x2 = max(lf[0], rf[0]) + 20 y = max(lf[1], rf[1]) + 2 # Deck (rounded rectangle) draw.rectangle((x1, y, x2, y + 8), fill=(120, 70, 150), outline=(60, 30, 90), width=2) draw.ellipse((x1 - 8, y - 1, x1 + 12, y + 9), fill=(120, 70, 150), outline=(60, 30, 90), width=2) draw.ellipse((x2 - 12, y - 1, x2 + 8, y + 9), fill=(120, 70, 150), outline=(60, 30, 90), width=2) # Wheels for wx in (x1 + 6, x2 - 6): draw.ellipse((wx - 5, y + 8, wx + 5, y + 18), fill=(50, 50, 50), outline=(20, 20, 20)) def _draw_controller_and_tv(draw: ImageDraw.ImageDraw, lh: Tuple[float, float], rh: Tuple[float, float]) -> None: """Game controller in the hands + a small glowing TV in front.""" # Controller: capsule shape between the hands cx = (lh[0] + rh[0]) / 2 cy = (lh[1] + rh[1]) / 2 # Body (rounded) draw.rectangle((cx - 24, cy - 8, cx + 24, cy + 10), fill=(45, 45, 55), outline=(180, 180, 200), width=2) draw.ellipse((cx - 34, cy - 10, cx - 14, cy + 14), fill=(45, 45, 55), outline=(180, 180, 200), width=2) draw.ellipse((cx + 14, cy - 10, cx + 34, cy + 14), fill=(45, 45, 55), outline=(180, 180, 200), width=2) # D-pad (left) draw.rectangle((cx - 26, cy - 2, cx - 20, cy + 4), fill=(200, 200, 210)) draw.rectangle((cx - 28, cy, cx - 18, cy + 2), fill=(200, 200, 210)) # A/B buttons (right) draw.ellipse((cx + 14, cy - 4, cx + 20, cy + 2), fill=(200, 60, 60)) draw.ellipse((cx + 20, cy + 2, cx + 26, cy + 8), fill=(60, 120, 200)) # TV in front of the skeleton (bright rectangle at upper area) tv_x1, tv_y1, tv_x2, tv_y2 = 120, 160, 392, 320 draw.rectangle((tv_x1, tv_y1, tv_x2, tv_y2), outline=(40, 40, 50), width=8, fill=(15, 20, 35)) draw.rectangle((tv_x1 + 6, tv_y1 + 6, tv_x2 - 6, tv_y2 - 6), fill=(40, 80, 160)) # Cartoon shapes on screen draw.ellipse((180, 210, 240, 270), fill=(240, 220, 80)) draw.polygon([(300, 240), (340, 200), (380, 260)], fill=(220, 60, 60)) def _draw_plant_tuft(draw: ImageDraw.ImageDraw, lh: Tuple[float, float], rh: Tuple[float, float]) -> None: # Little plant under the hands for gardening cx = (lh[0] + rh[0]) / 2 cy = max(lh[1], rh[1]) + 20 # Dirt patch draw.ellipse((cx - 40, cy, cx + 40, cy + 18), fill=(80, 50, 30)) # Stem + leaves draw.line((cx, cy + 8, cx, cy - 12), fill=(60, 120, 50), width=3) draw.polygon([(cx, cy - 4), (cx - 14, cy - 10), (cx - 4, cy - 2)], fill=(70, 140, 60)) draw.polygon([(cx, cy - 8), (cx + 14, cy - 14), (cx + 4, cy - 4)], fill=(70, 140, 60)) draw.polygon([(cx, cy - 12), (cx - 6, cy - 22), (cx + 6, cy - 22)], fill=(80, 160, 70)) def draw_frame( joints: Dict[int, Tuple[float, float]], action: str, emotion: str, scene: str = "none", ) -> Image.Image: img = Image.new("RGB", (CANVAS, CANVAS), BG_COLOR) draw = ImageDraw.Draw(img) # Scene backdrop (procedural — zero hallucination). scene_fn = SCENES.get(scene) if scene_fn is not None: scene_fn(draw) # Bones for a, b in SKELETON_EDGES: draw.line((joints[a], joints[b]), fill=BONE_COLOR, width=BONE_WIDTH) # Ribs + pelvis bowl _draw_ribs(draw, joints[NECK], joints[PELVIS]) _draw_pelvis(draw, joints[PELVIS]) # Joints for jid, (x, y) in joints.items(): if jid == HEAD: continue draw.ellipse( (x - JOINT_RADIUS, y - JOINT_RADIUS, x + JOINT_RADIUS, y + JOINT_RADIUS), fill=JOINT_COLOR, ) # Hands: angry => small filled fists if emotion == "angry": for hid in (LHA, RHA): hx, hy = joints[hid] draw.rectangle((hx - 7, hy - 7, hx + 7, hy + 7), fill=BONE_COLOR) # Props if action == "reading": _draw_book(draw, joints[LHA], joints[RHA]) elif action == "vacuuming": _draw_vacuum(draw, joints[LHA], joints[RHA]) elif action == "sweeping": _draw_broom(draw, joints[LHA], joints[RHA]) elif action == "cooking": _draw_pot(draw, joints[LHA], joints[RHA]) elif action == "washing": _draw_sink(draw, joints[LHA], joints[RHA]) elif action == "gardening": _draw_plant_tuft(draw, joints[LHA], joints[RHA]) elif action == "cleaning": _draw_spray_and_cloth(draw, joints[LHA], joints[RHA]) elif action == "dusting": _draw_feather_duster(draw, joints[RHA]) elif action == "mopping": _draw_mop(draw, joints[LHA], joints[RHA]) elif action == "ironing": _draw_iron_and_board(draw, joints[LHA], joints[RHA]) elif action == "polishing": _draw_polish_cloth(draw, joints[RHA]) elif action == "football": _draw_soccer_ball(draw, joints[RFT]) elif action == "cricket": _draw_cricket_bat(draw, joints[LHA], joints[RHA]) elif action == "basketball": _draw_basketball(draw, joints[RHA]) elif action == "tennis": _draw_tennis_racket(draw, joints[RHA]) elif action == "baseball": _draw_baseball_bat(draw, joints[LHA], joints[RHA]) elif action == "golf": _draw_golf_club(draw, joints[LHA], joints[RHA]) elif action == "bowling": _draw_bowling_ball(draw, joints[RHA]) elif action == "skateboarding": _draw_skateboard(draw, joints[LFT], joints[RFT]) elif action == "gaming": _draw_controller_and_tv(draw, joints[LHA], joints[RHA]) # Skull + face (drawn last so it sits above bones) _draw_skull(draw, joints[HEAD], emotion) return img # --------------------------------------------------------------------------- # Rendering loop + export # --------------------------------------------------------------------------- def render_gif(action: str, emotion: str, scene: str, out_path: str) -> str: if action not in ACTIONS: raise ValueError(f"Unknown action: {action}") if emotion not in EMOTION_LABELS: raise ValueError(f"Unknown emotion: {emotion}") if scene not in SCENE_LABELS: raise ValueError(f"Unknown scene: {scene}") logger.info("[render] action=%s emotion=%s scene=%s frames=%d fps=%d", action, emotion, scene, N_FRAMES, FPS) action_fn = ACTIONS[action] frames: list[Image.Image] = [] for i in range(N_FRAMES): t = i / N_FRAMES joints = action_fn(t) joints = apply_emotion(joints, emotion, t) img = draw_frame(joints, action, emotion, scene) frames.append(img) if i == 0 or i == N_FRAMES - 1: logger.debug("[render] frame %d/%d head=%s pelvis=%s", i + 1, N_FRAMES, joints[HEAD], joints[PELVIS]) Path(out_path).parent.mkdir(parents=True, exist_ok=True) logger.info("[export] writing %d frames -> %s (%dx%d, %d ms/frame)", len(frames), out_path, CANVAS, CANVAS, FRAME_MS) frames[0].save( out_path, save_all=True, append_images=frames[1:], duration=FRAME_MS, loop=0, disposal=2, optimize=False, ) return out_path # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- def _default_out_path(prompt: str, out_dir: str) -> str: ts = datetime.now().strftime("%Y%m%d_%H%M%S") slug = re.sub(r"[^a-z0-9]+", "_", prompt.lower()).strip("_")[:40] or "skeleton" return os.path.join(out_dir, f"{slug}-{ts}.gif") def main() -> int: parser = argparse.ArgumentParser( description="Generate a skeleton GIF from a text prompt (deterministic, no hallucination).", ) parser.add_argument("prompt", type=str, help="e.g. 'a sad man reading a book'") parser.add_argument("--out", "-o", type=str, default=None, help="Output .gif path or directory.") parser.add_argument("--debug", action="store_true", help="Verbose logging.") args = parser.parse_args() _setup_logging(args.debug) logger.debug("[main] argv=%s cwd=%s", sys.argv, os.getcwd()) prompt = args.prompt.strip() if not prompt: logger.error("Prompt cannot be empty.") return 1 try: action, emotion, scene = parse_prompt(prompt) except Exception as e: logger.error("[parse] FAILED: %s", e) logger.debug("%s", traceback.format_exc()) return 1 # Resolve output path project_root = Path(__file__).resolve().parent default_dir = str(project_root / "outputs") if args.out: if args.out.endswith(".gif"): out_path = args.out else: out_path = _default_out_path(prompt, args.out) else: out_path = _default_out_path(prompt, default_dir) try: t0 = time.time() render_gif(action, emotion, scene, out_path) logger.info("[done] saved %s in %.2fs", out_path, time.time() - t0) print(out_path) return 0 except Exception as e: logger.error("[render] FAILED: %s", e) logger.debug("%s", traceback.format_exc()) return 1 if __name__ == "__main__": raise SystemExit(main())