| """ |
| 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 |
|
|
| |
| |
| |
|
|
| 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", |
| ) |
| |
| 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)) |
|
|
|
|
| |
| |
| |
|
|
| CANVAS = 512 |
| N_FRAMES = 24 |
| FPS = 10 |
| FRAME_MS = int(1000 / FPS) |
|
|
| BG_COLOR = (8, 8, 14) |
| BONE_COLOR = (240, 240, 240) |
| JOINT_COLOR = (255, 255, 255) |
| ACCENT_COLOR = (200, 60, 60) |
| BONE_WIDTH = 6 |
| JOINT_RADIUS = 5 |
|
|
| FLOOR_Y = 462 |
|
|
| |
| 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: 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", |
| |
| "football", "cricket", "basketball", "tennis", "baseball", |
| "golf", "bowling", "skateboarding", "gaming", |
| |
| "bowing", "hugging", "kissing", "handshake", "pointing", |
| "climbing", "falling", "crawling", "rolling", "juggling", |
| |
| "swimming", "diving", "surfing", "skating", "skiing", "biking", |
| |
| "driving", "riding_horse", "rowing", |
| |
| "sneezing", "coughing", "yawning", "shivering", "scratching", |
| "shaving", "brushing_teeth", "combing_hair", "dressing", |
| |
| "carrying", "lifting", "pushing", "pulling", "throwing", "catching", |
| |
| "fishing", "shooting", "archery", "painting", "drawing", |
| "sculpting", "photographing", |
| |
| "singing", "whistling", "yelling", "teaching", "presenting", |
| |
| "playing_guitar", "playing_piano", "playing_drums", |
| |
| "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", |
| |
| "soccer_field", "cricket_ground", "basketball_court", |
| "tennis_court", "baseball_field", "golf_course", "bowling_alley", |
| |
| "classroom", "auditorium", "laboratory", |
| |
| "museum", "art_gallery", "theater", "cinema", "concert_hall", |
| "stadium", "zoo", "aquarium", |
| |
| "greenhouse", "cave", "mountain", "desert", "waterfall", "vineyard", |
| |
| "cemetery", "castle", "mansion", "cottage", "cabin", |
| "lighthouse", "temple", "monastery", |
| |
| "airport", "train_station", "subway", "bridge", |
| "parking_lot", "gas_station", |
| |
| "bank", "prison", "police_station", "fire_station", "courthouse", |
| |
| "factory", "warehouse", "construction_site", "garage", |
| |
| "basement", "attic", "laundry_room", "pantry", |
| |
| "swimming_pool", "casino", "nightclub", "arcade", "spa", |
| |
| "barn", "salon", "bakery", |
| "none", |
| ] |
|
|
| |
| |
| |
| ACTION_KEYWORDS: Dict[str, Tuple[str, ...]] = { |
| |
| "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": (), |
| "stretching": ("stretch", "yoga", "warm-up", "warmup", "limber", |
| "flex ", "loosen"), |
| |
| "fighting": ("fight", "punch", "box", "brawl", "attack", "combat", |
| "battle", "duel", "wrestle", "spar", "martial", |
| "strike", "assault"), |
| |
| "waving": ("wav", "hello", " hi ", "greet", "salute", "bye", |
| "farewell", "goodbye", "hail", "beckon"), |
| "clapping": ("clap", "applau", "ovation", "hand claps", "handclap"), |
| |
| "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"), |
| |
| "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"), |
| |
| "laughing": ("laugh", "giggl", "chuckl", "guffaw", "snicker", |
| "cackl", "lol", "rofl"), |
| "crying": ("cry", "weep", "sob", "tear", "bawl", "whimper", |
| "wail"), |
| |
| |
| "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"), |
| |
| |
| |
| "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",), |
| |
| "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"), |
| |
| "driving": ("driv", "steering wheel", "behind the wheel"), |
| "riding_horse": ("horseback", "riding a horse", "on horseback", |
| "equestrian"), |
| "rowing": ("row ", "rowing", "oar ", "canoe"), |
| |
| "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"), |
| |
| "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"), |
| |
| "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"), |
| |
| "singing": ("sing ", "singing", "sung", "=sings", "croon"), |
| "whistling": ("whistl",), |
| "yelling": ("yell", "shout", "scream"), |
| "teaching": ("teach", "lectur", "instruct"), |
| "presenting": ("present ", "presenting", "keynote", "pitch to"), |
| |
| "playing_guitar": ("guitar", "strum", "bass guitar", "ukulele"), |
| "playing_piano": ("piano", "keyboard play", "keys play"), |
| "playing_drums": ("drum", "drumkit", "drum kit", "percuss"), |
| |
| "shopping": ("shopping", "grocery run", "shopping cart", |
| "browsing aisles"), |
| "texting": ("text", "texting", "sms", "whatsapp", "messag"), |
| "meditating": ("meditat", "=zen", "mindful", "namaste"), |
| |
| "vacuuming": ("vacuum", "vaccum", "vaccu", "vaccui", "hoover", |
| "hoovering"), |
| "sweeping": ("sweep", "broom"), |
| "cooking": ("cook", "bake", "boil", "fry", "recipe", "stir ", |
| "simmer", "chef", "prepar"), |
| |
| "washing": ("wash", "dish", "scrub", "launder", "laundry", |
| "rins"), |
| |
| "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"), |
| |
| "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"), |
| |
| "working": ("work", "typ", "comput", "keyboard", "laptop", "task", |
| "labor", "busy ", "grind", "employ", "job ", "offic", |
| "market", " cook", "shop"), |
| } |
| |
| 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, ...]] = { |
| |
| "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"), |
| |
| "classroom": ("classroom", "=class", "lecture hall"), |
| "auditorium": ("auditorium", "assembly hall"), |
| "laboratory": ("laboratory", "=lab ", "=labs"), |
| |
| "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"), |
| |
| "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"), |
| |
| "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"), |
| |
| "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"), |
| |
| "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"), |
| |
| "factory": ("factory", "plant floor", "assembly line", |
| "manufacturing"), |
| "warehouse": ("warehouse", "stockroom", "depot"), |
| "construction_site": ("construction site", "building site", |
| "scaffolding"), |
| "garage": ("=garage", "=garages", "repair shop"), |
| |
| "basement": ("basement", "cellar"), |
| "attic": ("=attic", "=attics", "loft "), |
| "laundry_room": ("laundry room", "washer dryer"), |
| "pantry": ("pantry", "larder"), |
| |
| "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"), |
| |
| "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"), |
| } |
|
|
|
|
| |
| |
| |
|
|
| 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 |
|
|
| global _ZS_PIPE |
| 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 |
|
|
|
|
| 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) |
|
|
| |
| |
| |
| 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: |
| 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: |
| |
| 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 |
|
|
|
|
| |
| |
| |
|
|
| 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_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) |
| |
| 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) |
| |
| 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) |
| |
| for j in (HEAD, NECK, TORSO): |
| _shift(p, j, 10, 0) |
| return p |
|
|
|
|
| def action_dancing(t: float) -> Dict[int, Tuple[float, float]]: |
| p = _copy_pose() |
| phase = 2 * math.pi * t |
| |
| 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)) |
| |
| sway = 10 * math.sin(phase) |
| for j in (PELVIS, LHP, RHP, LKN, LFT, RKN, RFT): |
| _shift(p, j, sway, 0) |
| |
| for j in (HEAD, NECK, TORSO, LSH, RSH): |
| _shift(p, j, -sway * 0.4, 0) |
| |
| 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() |
| |
| _shift(p, HEAD, 0, 18) |
| _shift(p, NECK, 0, 10) |
| _shift(p, TORSO, 0, 5) |
| |
| p[LEL] = (230, 230) |
| p[LHA] = (240, 260) |
| p[REL] = (282, 230) |
| p[RHA] = (272, 260) |
| |
| 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 |
| |
| 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) |
| |
| _shift(p, HEAD, 3, -2) |
| return p |
|
|
|
|
| def action_sitting(t: float) -> Dict[int, Tuple[float, float]]: |
| p = _copy_pose() |
| drop = 40 |
| |
| for j in (HEAD, NECK, TORSO, PELVIS, LSH, RSH, LHP, RHP): |
| _shift(p, j, 0, drop) |
| |
| p[LKN] = (220, 360 + drop) |
| p[RKN] = (292, 360 + drop) |
| p[LFT] = (180, 380 + drop) |
| p[RFT] = (330, 380 + drop) |
| |
| 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)) |
| |
| for j in p: |
| _shift(p, j, 0, -lift) |
| |
| 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) |
| |
| 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 |
| |
| for j in (HEAD, NECK, TORSO): |
| _shift(p, j, 6, 6) |
| |
| 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() |
| |
| p[REL] = (280, 180) |
| p[RHA] = (260, 140) |
| |
| 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 |
| |
| for j in (HEAD, NECK, TORSO): |
| _shift(p, j, 0, 8) |
| |
| 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) |
| |
| p[REL] = (p[HEAD][0] + 22, p[HEAD][1] + 18) |
| p[RHA] = (p[HEAD][0] + 6, p[HEAD][1] + 6 - lift * 0.2) |
| |
| 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) |
| |
| _shift(p, HEAD, 0, -2 - abs(tilt)) |
| |
| 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]]: |
| |
| 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(): |
| |
| 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) |
| |
| _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) |
| |
| 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) |
| |
| _shift(p, HEAD, shake * 0.5, -4) |
| _shift(p, NECK, shake * 0.3, -1) |
| _shift(p, TORSO, shake * 0.4, 0) |
| |
| 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() |
| |
| _shift(p, HEAD, 0, 6 + 1.5 * math.sin(2 * math.pi * t)) |
| _shift(p, NECK, 0, 3) |
| |
| 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 |
| |
| 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) |
| |
| 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 |
| |
| for j in (HEAD, NECK, TORSO): |
| _shift(p, j, 0, 8) |
| |
| p[LEL] = (228, 240) |
| p[LHA] = (238, 272) |
| |
| 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) |
| |
| 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) |
| |
| _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 |
| |
| for j in (HEAD, NECK, TORSO): |
| _shift(p, j, 10, 6) |
| |
| |
| 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 |
| |
| 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 |
| |
| for j in (HEAD, NECK, TORSO): |
| _shift(p, j, 0, 8) |
| |
| p[LEL] = (228, 245) |
| p[LHA] = (222, 290) |
| |
| 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 |
| |
| for j in (HEAD, NECK, TORSO): |
| _shift(p, j, 0, 10) |
| |
| 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 |
| |
| for j in (HEAD, NECK, TORSO): |
| _shift(p, j, 10, 30) |
| _shift(p, PELVIS, 0, 20) |
| |
| p[LKN] = (228, 380) |
| p[RKN] = (284, 380) |
| p[LFT] = (222, 438) |
| p[RFT] = (290, 438) |
| |
| 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)) |
| |
| 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) |
| |
| 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) |
| |
| for j in (HEAD, NECK, TORSO): |
| _shift(p, j, 0, 6) |
| |
| p[LEL] = (232, 230) |
| p[LHA] = (226, 260) |
| |
| 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) |
| |
| _shift(p, HEAD, 0, -2) |
| |
| 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) |
| |
| 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) |
| |
| for j in (HEAD, NECK, TORSO): |
| _shift(p, j, 0, 6) |
| |
| p[REL] = (266, 250) |
| p[RHA] = (266 + slide, 288) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| p[LKN] = (p[LHP][0] - 6, p[LHP][1] + 65) |
| p[LFT] = (p[LHP][0] - 8, p[LHP][1] + 130) |
| |
| 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) |
| |
| 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) |
| |
| for j in (HEAD, NECK, TORSO): |
| _shift(p, j, -4, 2) |
| |
| p[LEL] = (250, 220) |
| p[REL] = (275, 210) |
| p[LHA] = (286 + 8 * swing, 235 - 6 * swing) |
| p[RHA] = (296 + 8 * swing, 230 - 6 * swing) |
| |
| 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 |
| |
| for j in (HEAD, NECK, TORSO): |
| _shift(p, j, 6, 8) |
| |
| 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) |
| |
| p[LEL] = (p[LSH][0] - 15, p[LSH][1] + 35) |
| p[LHA] = (p[LSH][0] - 30, p[LSH][1] + 55) |
| |
| 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 = math.sin(phase) |
| |
| 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) |
| |
| p[LEL] = (p[LSH][0] - 20 - 10 * swing, p[LSH][1] + 10) |
| p[LHA] = (p[LSH][0] - 35 - 20 * swing, p[LSH][1] + 25) |
| |
| 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 |
| |
| for j in (HEAD, NECK, TORSO): |
| _shift(p, j, -3 + 4 * windup, 4) |
| |
| 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) |
| |
| 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) |
| |
| for j in (HEAD, NECK, TORSO): |
| _shift(p, j, 0, 10) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| p[LEL] = (p[LSH][0] - 20, p[LSH][1] + 5) |
| p[LHA] = (p[LSH][0] - 35, p[LSH][1] - 5) |
| |
| 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) |
| |
| for j in (HEAD, NECK, TORSO): |
| _shift(p, j, lean, 6) |
| |
| 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) |
| |
| 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 |
|
|
|
|
| |
| |
| |
| |
| |
|
|
| 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) |
| |
| p[LEL] = (p[LSH][0] - 10, p[LSH][1] - 30 + 10 * up) |
| p[LHA] = (p[LSH][0] - 6, p[LSH][1] - 80 + 10 * up) |
| |
| p[REL] = (p[RSH][0] + 15, p[RSH][1] + 10 - 10 * up) |
| p[RHA] = (p[RSH][0] + 10, p[RSH][1] + 50 - 10 * up) |
| |
| 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]]: |
| |
| 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) |
| |
| 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 |
| |
| 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) |
| |
| 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]]: |
| |
| 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 |
| |
| 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]]: |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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 |
| |
| 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) |
| |
| 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) |
| |
| p[LEL] = (236, 250 + drop); p[REL] = (276, 250 + drop) |
| p[LHA] = (232, 290 + drop); p[RHA] = (280, 290 + drop) |
| |
| 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) |
| |
| p[LEL] = (236, 250 + drop); p[REL] = (276, 250 + drop) |
| p[LHA] = (240, 285 + drop); p[RHA] = (272, 285 + drop) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| p[LKN] = (224, 380 + drop); p[RKN] = (288, 380 + drop) |
| p[LFT] = (194, 410 + drop); p[RFT] = (318, 410 + drop) |
| |
| 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) |
| |
| p[LEL] = (p[LSH][0] - 20, p[LSH][1] + 25) |
| p[LHA] = (p[LSH][0] - 30, p[LSH][1] + 55) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| p[LHP] = (232, 330); p[RHP] = (280, 330) |
| p[LKN] = (190, 370); p[RKN] = (322, 370) |
| p[LFT] = (250, 380); p[RFT] = (262, 380) |
| |
| 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 |
| |
| drop = 30 |
| for j in (HEAD, NECK, TORSO, PELVIS, LSH, RSH, LHP, RHP): |
| _shift(p, j, 0, drop) |
| |
| _shift(p, HEAD, 0, 6) |
| |
| 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) |
| |
| 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, |
| |
| "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, |
| |
| "swimming": action_swimming, |
| "diving": action_diving, |
| "surfing": action_surfing, |
| "skating": action_skating, |
| "skiing": action_skiing, |
| "biking": action_biking, |
| |
| "driving": action_driving, |
| "riding_horse": action_riding_horse, |
| "rowing": action_rowing, |
| |
| "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, |
| |
| "carrying": action_carrying, |
| "lifting": action_lifting, |
| "pushing": action_pushing, |
| "pulling": action_pulling, |
| "throwing": action_throwing, |
| "catching": action_catching, |
| |
| "fishing": action_fishing, |
| "shooting": action_shooting, |
| "archery": action_archery, |
| "painting": action_painting, |
| "drawing": action_drawing, |
| "sculpting": action_sculpting, |
| "photographing": action_photographing, |
| |
| "singing": action_singing, |
| "whistling": action_whistling, |
| "yelling": action_yelling, |
| "teaching": action_teaching, |
| "presenting": action_presenting, |
| |
| "playing_guitar": action_playing_guitar, |
| "playing_piano": action_playing_piano, |
| "playing_drums": action_playing_drums, |
| |
| "shopping": action_shopping, |
| "texting": action_texting, |
| "meditating": action_meditating, |
| } |
|
|
|
|
| |
| |
| |
|
|
| 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: |
| 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": |
| |
| 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": |
| |
| 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": |
| |
| for j in p: |
| 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": |
| |
| tilt = 6 |
| _shift(p, HEAD, tilt, 2) |
| _shift(p, NECK, tilt * 0.4, 1) |
| _shift(p, RSH, 0, -4) |
| |
| _shift(p, REL, 0, -20); _shift(p, RHA, -20, -30) |
| |
| return p |
|
|
|
|
| |
| |
| |
|
|
| 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) |
| |
| for y in range(FLOOR_Y + 12, CANVAS, 16): |
| draw.line((0, y, CANVAS, y), fill=(50, 30, 22), width=1) |
| |
| 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) |
| |
| 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)) |
| |
| draw.ellipse((135, 100, 170, 135), fill=(240, 230, 200)) |
| |
| bed_x1, bed_y1, bed_x2, bed_y2 = 300, 400, 504, 458 |
| |
| draw.rectangle((bed_x2 - 18, 330, bed_x2 + 6, bed_y1 + 10), |
| fill=(90, 55, 30), outline=(160, 110, 70), width=2) |
| |
| draw.arc((bed_x2 - 22, 318, bed_x2 + 10, 350), |
| start=180, end=360, fill=(200, 150, 100), width=3) |
| |
| draw.rectangle((bed_x1 - 4, 395, bed_x1 + 10, bed_y2 + 8), |
| fill=(90, 55, 30), outline=(160, 110, 70), width=2) |
| |
| draw.rectangle((bed_x1, bed_y1 + 14, bed_x2, bed_y2 + 8), |
| fill=(80, 50, 28), outline=(160, 110, 70), width=2) |
| |
| draw.rectangle((bed_x1 + 4, bed_y1 + 6, bed_x2 - 4, bed_y1 + 16), |
| fill=(230, 225, 210), outline=(180, 170, 150), width=1) |
| |
| draw.rectangle((bed_x1, bed_y1, bed_x2 - 4, bed_y1 + 28), |
| fill=(120, 50, 80), outline=(180, 100, 130), width=2) |
| |
| draw.rectangle((bed_x1 + 78, bed_y1 - 4, bed_x2 - 4, bed_y1 + 8), |
| fill=(200, 200, 215), outline=(140, 140, 160), width=1) |
| |
| 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) |
| |
| ns_x1, ns_x2 = 240, 290 |
| draw.rectangle((ns_x1, 408, ns_x2, 462), |
| fill=(72, 48, 30), outline=(150, 110, 70), width=2) |
| |
| 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_cx = (ns_x1 + ns_x2) // 2 |
| lamp_base_y = 408 |
| |
| 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) |
| |
| draw.line((lamp_cx, lamp_base_y - 8, lamp_cx, lamp_base_y - 40), |
| fill=(200, 180, 120), width=3) |
| |
| 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)) |
| |
| draw.ellipse((lamp_cx - 42, lamp_base_y - 46, |
| lamp_cx + 42, lamp_base_y - 18), |
| fill=None, outline=(250, 220, 140), width=1) |
| |
| draw.rectangle((160, 475, 360, 505), fill=(100, 60, 60), |
| outline=(200, 160, 110), width=2) |
| |
| 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) |
| |
| 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) |
| |
| for x in (60, 180, 340, 460): |
| draw.line((x, 200, x, FLOOR_Y), fill=(120, 80, 50), width=4) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| draw.ellipse((400, 60, 470, 130), fill=(240, 220, 120)) |
| |
| 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)) |
| |
| 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) |
| |
| 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): |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| draw.rectangle((432, 240, 468, 258), fill=(140, 80, 50)) |
| draw.ellipse((430, 220, 470, 248), fill=(70, 130, 60)) |
| |
| 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) |
| |
| 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) |
| |
| draw.rectangle((290, 330, 400, 380), |
| outline=(90, 90, 95), width=2, fill=(45, 45, 50)) |
| |
| for kx in (300, 320, 370, 390): |
| draw.ellipse((kx - 3, 335, kx + 3, 341), fill=(200, 200, 210)) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| draw.line((110, 220, 110, 270), fill=(150, 160, 170), width=3) |
| draw.line((110, 310, 110, 380), fill=(150, 160, 170), width=3) |
| |
| 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) |
| |
| for x in range(20, CANVAS, 60): |
| draw.rectangle((x, 485, x + 30, 492), fill=(220, 210, 140)) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| draw.rectangle((80, 80, 440, 280), outline=(180, 200, 210), width=4, fill=(70, 85, 100)) |
| |
| 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) |
| |
| 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) |
| |
| for y in range(FLOOR_Y + 14, CANVAS, 18): |
| draw.line((0, y, CANVAS, y), fill=(85, 60, 40), width=1) |
| |
| draw.line((0, FLOOR_Y - 2, CANVAS, FLOOR_Y - 2), fill=(180, 150, 110), width=2) |
| |
| 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)) |
| |
| draw.ellipse((160, 130, 180, 150), fill=(240, 210, 120)) |
| |
| draw.rectangle((330, 150, 490, 270), |
| outline=(40, 40, 50), width=6, fill=(15, 20, 35)) |
| |
| draw.rectangle((340, 160, 480, 260), fill=(30, 50, 90)) |
| draw.polygon([(360, 200), (420, 180), (460, 220), (380, 250)], |
| fill=(80, 120, 180)) |
| |
| draw.rectangle((340, 280, 480, 310), |
| fill=(80, 55, 35), outline=(150, 100, 60), width=2) |
| |
| sofa_x1, sofa_y1, sofa_x2, sofa_y2 = 40, 340, 270, FLOOR_Y - 4 |
| |
| draw.rectangle((sofa_x1, sofa_y1 - 30, sofa_x2, sofa_y1 + 10), |
| fill=(100, 55, 90), outline=(160, 100, 140), width=2) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| for fx in (sofa_x1 + 6, sofa_x2 - 10): |
| draw.rectangle((fx, sofa_y2, fx + 6, sofa_y2 + 8), |
| fill=(60, 40, 25)) |
| |
| 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) |
| |
| 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_x1, rug_y1, rug_x2, rug_y2 = 90, 470, 430, 505 |
| |
| draw.rectangle((rug_x1, rug_y1, rug_x2, rug_y2), |
| fill=(150, 75, 55), outline=(230, 180, 110), width=3) |
| |
| draw.rectangle((rug_x1 + 10, rug_y1 + 5, rug_x2 - 10, rug_y2 - 5), |
| outline=(230, 180, 110), width=2) |
| |
| 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)) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| draw.ellipse((380, 60, 450, 130), fill=(250, 220, 120)) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| draw.rectangle((60, 80, 450, 260), fill=(35, 60, 50), outline=(90, 60, 30), width=4) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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)) |
| |
| 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)) |
| draw.rectangle((485, 370, 500, 440), fill=(180, 180, 185)) |
| |
| draw.rectangle((300, 370, 360, 395), fill=(255, 255, 255), outline=(180, 180, 180), width=1) |
| |
| 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) |
| |
| 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) |
| |
| draw.rectangle((160, 100, 350, 240), outline=(140, 140, 140), width=4, fill=(150, 170, 180)) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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)) |
| |
| 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)) |
| |
| 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) |
| |
| draw.ellipse((20, 420, 160, 560), fill=(120, 70, 40), outline=(180, 100, 60), width=2) |
| |
| draw.ellipse((390, 80, 460, 150), fill=(220, 220, 230)) |
| draw.ellipse((405, 80, 470, 150), fill=(4, 4, 16)) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| 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) |
| |
| draw.ellipse((60, 80, 130, 150), fill=(250, 220, 130)) |
| |
| 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)) |
| |
| 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)) |
| |
| draw.rectangle((360, 400, 410, FLOOR_Y), fill=(70, 40, 20), |
| outline=(20, 20, 20), width=2) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| draw.line((0, 340, CANVAS, 340), fill=(240, 240, 240), width=2) |
| draw.ellipse((180, 320, 332, 360), |
| outline=(240, 240, 240), width=2) |
| |
| draw.rectangle((30, 310, 130, 352), |
| outline=(240, 240, 240), width=3) |
| draw.line((30, 310, 130, 310), fill=(240, 240, 240), width=3) |
| |
| 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) |
| |
| 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) |
| |
| draw.ellipse((-60, 340, CANVAS + 60, 540), |
| outline=(255, 255, 255), width=3) |
| |
| 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)) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| for y in range(FLOOR_Y + 8, CANVAS, 14): |
| draw.line((0, y, CANVAS, y), fill=(150, 100, 55), width=1) |
| |
| draw.polygon([(180, FLOOR_Y + 4), (332, FLOOR_Y + 4), |
| (370, CANVAS - 2), (142, CANVAS - 2)], |
| outline=(230, 230, 230), width=3, fill=None) |
| |
| draw.arc((195, FLOOR_Y - 4, 317, FLOOR_Y + 38), |
| start=0, end=180, fill=(230, 230, 230), width=3) |
| |
| draw.rectangle((196, 180, 316, 260), |
| fill=(240, 240, 250), outline=(80, 80, 100), width=4) |
| |
| draw.rectangle((232, 210, 280, 244), |
| outline=(200, 40, 40), width=3) |
| |
| draw.ellipse((226, 256, 286, 276), |
| outline=(220, 80, 40), width=4) |
| |
| 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) |
| |
| 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) |
| out = (75, 110, 55) |
| draw.rectangle((0, 0, CANVAS, 300), fill=sky) |
| draw.rectangle((0, 300, CANVAS, CANVAS), fill=out) |
| |
| draw.polygon([(90, 330), (422, 330), (500, CANVAS), (12, CANVAS)], |
| fill=court, outline=(240, 240, 240)) |
| |
| 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) |
| |
| draw.line((256, 330, 256, CANVAS), |
| fill=(240, 240, 240), width=2) |
| |
| draw.line((130, 390, 382, 390), fill=(240, 240, 240), width=2) |
| |
| 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) |
| |
| draw.line((80, 300, 80, 340), fill=(200, 200, 200), width=3) |
| draw.line((432, 300, 432, 340), fill=(200, 200, 200), width=3) |
| |
| 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) |
| |
| draw.polygon([(256, 310), (460, 420), (256, CANVAS - 4), (52, 420)], |
| fill=infield, outline=(240, 240, 240)) |
| |
| 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)) |
| |
| draw.ellipse((236, 390, 276, 420), fill=(200, 155, 100), |
| outline=(140, 100, 60)) |
| |
| 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)) |
| |
| 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) |
| |
| 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)) |
| |
| draw.ellipse((400, 80, 460, 140), fill=(250, 230, 140)) |
| |
| draw.polygon([(140, 300), (372, 300), (470, CANVAS), (42, CANVAS)], |
| fill=fairway) |
| |
| draw.ellipse((90, 400, 210, 450), fill=sand, outline=(180, 150, 90)) |
| |
| 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)) |
| |
| 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) |
| |
| draw.polygon([(120, FLOOR_Y), (392, FLOOR_Y), |
| (310, 200), (202, 200)], |
| fill=lane, outline=(120, 85, 45), width=2) |
| |
| 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) |
| |
| 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) |
| |
| draw.rectangle((190, 185, 322, 215), |
| fill=(130, 100, 60), outline=(80, 60, 30), width=2) |
| |
| 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 |
| |
| draw.ellipse((px - 4, py - 12, px + 4, py + 4), |
| fill=(250, 250, 250), outline=(180, 180, 180)) |
| |
| draw.line((px - 3, py - 7, px + 3, py - 7), |
| fill=(200, 40, 40), width=2) |
| |
| 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)) |
|
|
|
|
| |
| |
| |
| |
|
|
| 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)) |
| |
| 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) |
| |
| 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) |
| |
| 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)) |
| |
| draw.rectangle((20, 300, 490, 400), fill=(120, 60, 70), outline=(200, 150, 100), width=3) |
| |
| 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)) |
| |
| draw.rectangle((236, 330, 276, 390), fill=(160, 120, 70), outline=(90, 60, 30), width=2) |
| |
| draw.polygon([(256, 80), (200, 330), (312, 330)], fill=(255, 240, 180)) |
| |
| 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)) |
| |
| 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) |
| |
| 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)) |
| |
| for i, (cx, h, col) in enumerate([(280, 30, (80, 200, 120)), |
| (330, 50, (200, 80, 100)), |
| (380, 40, (80, 120, 220))]): |
| |
| 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) |
| |
| draw.ellipse((cx - 2, 380 - h + 10, cx + 2, 380 - h + 14), |
| fill=(230, 240, 250)) |
| |
| 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)) |
| |
| 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) |
| |
| 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)) |
| |
| 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)) |
| |
| 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) |
| |
| 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)) |
| |
| 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)) |
| |
| 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) |
| |
| 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) |
| |
| draw.rectangle((60, 400, 452, FLOOR_Y), fill=(90, 60, 40), outline=(160, 110, 70), width=2) |
| |
| 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)) |
| |
| draw.rectangle((60, 80, 450, 280), fill=(240, 240, 250), |
| outline=(80, 80, 100), width=6) |
| |
| draw.polygon([(80, 260), (180, 160), (280, 260)], fill=(60, 100, 60)) |
| draw.ellipse((330, 110, 400, 180), fill=(250, 230, 140)) |
| |
| draw.polygon([(200, 330), (312, 330), (400, CANVAS), (112, CANVAS)], |
| fill=(70, 30, 45)) |
| |
| 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)) |
| |
| draw.rectangle((50, 380, 462, FLOOR_Y), fill=(120, 80, 50), outline=(180, 130, 80), width=2) |
| |
| 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)) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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)) |
| |
| draw.line((0, 330, CANVAS, 330), fill=(240, 240, 240), width=3) |
| |
| 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)) |
| |
| 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) |
| |
| 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) |
| |
| for lx in (110, 140, 170, 200): |
| draw.rectangle((lx, 370, lx + 10, 400), fill=(100, 90, 100)) |
| |
| 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)) |
| |
| draw.rectangle((40, 80, 472, 420), outline=(140, 160, 190), width=6, |
| fill=(40, 100, 150)) |
| |
| 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) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| draw.polygon([(0, 0), (CANVAS, 0), (CANVAS - 30, 60), (30, 60)], |
| fill=(140, 180, 170)) |
| |
| 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)) |
| |
| 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)) |
| |
| 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)) |
| |
| 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)) |
| |
| 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)) |
| |
| 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)) |
| |
| 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)) |
| |
| 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) |
| |
| draw.ellipse((380, 90, 460, 170), fill=(250, 210, 100)) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| 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) |
| |
| draw.ellipse((160, FLOOR_Y - 20, 340, FLOOR_Y + 40), |
| fill=(120, 180, 220)) |
| |
| 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)) |
| |
| 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)) |
| |
| 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)) |
| |
| draw.ellipse((400, 90, 450, 140), fill=(230, 230, 210)) |
| |
| 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) |
| |
| 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)) |
| |
| draw.rectangle((120, 180, 392, 440), fill=(160, 150, 130), |
| outline=(90, 80, 60), width=3) |
| |
| for x in range(120, 392, 30): |
| draw.rectangle((x, 160, x + 15, 180), fill=(160, 150, 130), |
| outline=(90, 80, 60), width=2) |
| |
| 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)) |
| |
| 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) |
| |
| 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)) |
| |
| 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)) |
| |
| 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)) |
| |
| 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)) |
| |
| for y in range(80, FLOOR_Y, 30): |
| draw.line((0, y, CANVAS, y), fill=(140, 110, 70), width=1) |
| |
| 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)) |
| |
| 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) |
| |
| draw.rectangle((180, 250, 332, 270), fill=(120, 80, 50), |
| outline=(80, 50, 20), width=2) |
| |
| 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)) |
| |
| 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)) |
| |
| 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)) |
| |
| draw.rectangle((220, 320, 292, 420), fill=(30, 20, 15)) |
| |
| 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)) |
| |
| 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) |
| |
| draw.ellipse((200, 60, 312, 140), fill=(250, 220, 140), |
| outline=(200, 160, 70), width=3) |
| |
| draw.polygon([(256, 100), (60, 20), (60, 180)], fill=(255, 240, 170, 120)) |
| |
| 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)) |
| |
| 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) |
| |
| draw.polygon([(20, 100), (CANVAS - 20, 100), (CANVAS - 60, 60), (60, 60)], |
| fill=(130, 40, 40), outline=(80, 20, 25), width=2) |
| |
| 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) |
| |
| 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)) |
| |
| 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)) |
| |
| for y in range(FLOOR_Y + 10, CANVAS, 20): |
| draw.line((0, y, CANVAS, y), fill=(70, 55, 40), width=1) |
| |
| 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)) |
| |
| 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) |
| |
| 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)) |
| |
| draw.line((0, FLOOR_Y - 30, CANVAS, FLOOR_Y - 30), fill=(220, 210, 80), width=3) |
| |
| 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)) |
| |
| draw.rectangle((0, FLOOR_Y - 10, CANVAS, FLOOR_Y), fill=(220, 210, 80)) |
| |
| draw.rectangle((40, 300, 472, FLOOR_Y - 14), |
| fill=(120, 30, 40), outline=(60, 15, 20), width=3) |
| |
| for x in range(60, 470, 50): |
| draw.rectangle((x, 330, x + 30, 370), fill=(160, 200, 230), |
| outline=(40, 10, 20), width=2) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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)) |
| |
| draw.arc((60, 120, 462, 360), start=180, end=360, |
| fill=(40, 40, 50), width=4) |
| |
| 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) |
| |
| 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)) |
| |
| for x in range(20, CANVAS, 56): |
| draw.line((x, 320, x, FLOOR_Y), fill=(230, 230, 80), width=2) |
| |
| 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)) |
| |
| 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)) |
| |
| 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)) |
| |
| for px in (180, 332): |
| draw.rectangle((px - 15, 340, px + 15, 440), |
| fill=(230, 230, 230), outline=(100, 100, 110), width=2) |
| |
| draw.rectangle((px - 10, 350, px + 10, 380), fill=(60, 80, 60)) |
| |
| draw.line((px + 15, 370, px + 35, 380), fill=(60, 60, 70), width=3) |
| |
| 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)) |
| |
| 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)) |
| |
| for x in range(80, 440, 90): |
| draw.rectangle((x, 280, x + 60, 340), outline=(80, 50, 20), width=3, |
| fill=(200, 220, 230)) |
| |
| 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)) |
| |
| 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)) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| 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)) |
| |
| 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)) |
| |
| 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) |
| |
| draw.rectangle((120, 360, 392, 430), fill=(90, 60, 35), |
| outline=(150, 100, 55), width=3) |
| |
| 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) |
| |
| 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)) |
| |
| draw.rectangle((30, 100, 90, FLOOR_Y), fill=(150, 80, 60), |
| outline=(80, 40, 30), width=3) |
| |
| for sx, sy in ((40, 80), (60, 60), (50, 40)): |
| draw.ellipse((sx - 15, sy - 15, sx + 15, sy + 15), |
| fill=(120, 120, 130)) |
| |
| 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) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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)) |
| |
| 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)) |
| |
| 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)) |
| |
| 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) |
| |
| draw.rectangle((70, 290, 150, 320), fill=(200, 40, 40), |
| outline=(130, 20, 20), width=2) |
| |
| 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)) |
| |
| for x in range(80, CANVAS, 100): |
| draw.rectangle((x, 60, x + 20, 120), fill=(100, 70, 40), |
| outline=(60, 40, 20), width=2) |
| |
| 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) |
| |
| draw.line((200, 300, CANVAS, 300), fill=(160, 110, 40), width=8) |
| draw.line((200, 340, CANVAS, 340), fill=(160, 110, 40), width=8) |
| |
| 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)) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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)) |
| |
| 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)) |
| |
| 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)) |
| |
| 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)) |
| |
| 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)) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| draw.rectangle((30, 300, 482, FLOOR_Y), fill=(60, 160, 200), |
| outline=(40, 110, 150), width=4) |
| |
| 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) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| draw.ellipse((60, 300, 452, 460), fill=(30, 90, 60), |
| outline=(200, 170, 80), width=5) |
| |
| 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) |
| |
| 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)) |
| |
| 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)) |
| |
| draw.line((256, 0, 256, 70), fill=(80, 80, 90), width=2) |
| |
| 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) |
| |
| draw.rectangle((180, 340, 332, 410), fill=(40, 30, 60), |
| outline=(180, 80, 200), width=3) |
| |
| for tx in (216, 296): |
| draw.ellipse((tx - 16, 350, tx + 16, 382), outline=(200, 200, 220), width=2, |
| fill=(20, 10, 30)) |
| |
| 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)) |
| |
| 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) |
| |
| draw.rectangle((x + 10, 150, x + 80, 230), fill=(30, 30, 50), |
| outline=(220, 220, 240), width=2) |
| |
| draw.rectangle((x + 20, 190, x + 30, 200), fill=(240, 240, 100)) |
| draw.ellipse((x + 40, 170, x + 60, 190), fill=(240, 80, 120)) |
| |
| 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)) |
| |
| 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)) |
| |
| 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) |
| |
| 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) |
| |
| draw.ellipse((140, 380, 170, 410), fill=(180, 160, 140)) |
| |
| 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)) |
| |
| 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)) |
| |
| for x in range(0, CANVAS, 30): |
| draw.line((x, 60, x, FLOOR_Y), fill=(60, 20, 15), width=2) |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| 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)) |
| |
| draw.rectangle((60, 100, 300, 360), outline=(200, 170, 100), width=6, |
| fill=(180, 200, 210)) |
| |
| draw.line((80, 160, 280, 180), fill=(240, 245, 250), width=2) |
| draw.line((80, 220, 280, 240), fill=(240, 245, 250), width=2) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| 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)) |
| |
| 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) |
| |
| 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, |
| |
| "classroom": _draw_scene_classroom, |
| "auditorium": _draw_scene_auditorium, |
| "laboratory": _draw_scene_laboratory, |
| |
| "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, |
| |
| "greenhouse": _draw_scene_greenhouse, |
| "cave": _draw_scene_cave, |
| "mountain": _draw_scene_mountain, |
| "desert": _draw_scene_desert, |
| "waterfall": _draw_scene_waterfall, |
| "vineyard": _draw_scene_vineyard, |
| |
| "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, |
| |
| "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, |
| |
| "bank": _draw_scene_bank, |
| "prison": _draw_scene_prison, |
| "police_station": _draw_scene_police_station, |
| "fire_station": _draw_scene_fire_station, |
| "courthouse": _draw_scene_courthouse, |
| |
| "factory": _draw_scene_factory, |
| "warehouse": _draw_scene_warehouse, |
| "construction_site": _draw_scene_construction_site, |
| "garage": _draw_scene_garage, |
| |
| "basement": _draw_scene_basement, |
| "attic": _draw_scene_attic, |
| "laundry_room": _draw_scene_laundry_room, |
| "pantry": _draw_scene_pantry, |
| |
| "swimming_pool": _draw_scene_swimming_pool, |
| "casino": _draw_scene_casino, |
| "nightclub": _draw_scene_nightclub, |
| "arcade": _draw_scene_arcade, |
| "spa": _draw_scene_spa, |
| |
| "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 |
| |
| draw.ellipse((cx - r, cy - r, cx + r, cy + r), outline=BONE_COLOR, width=3, fill=BG_COLOR) |
| |
| 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) |
|
|
| |
| 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": |
| |
| 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": |
| |
| 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": |
| |
| 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": |
| |
| 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) |
| |
| 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) |
|
|
| |
| nose_y = cy + 4 |
| draw.polygon([(cx, nose_y), (cx - 3, nose_y + 8), (cx + 3, nose_y + 8)], outline=BONE_COLOR) |
|
|
| |
| 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) |
| |
| 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": |
| |
| 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": |
| |
| 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": |
| |
| draw.ellipse((cx - mw // 3, my - 4, cx + mw // 3, my + 10), outline=BONE_COLOR, width=3, fill=BG_COLOR) |
| elif emotion == "bored": |
| |
| draw.line((cx - mw // 2, my + 2, cx + mw // 2, my + 6), fill=BONE_COLOR, width=2) |
| elif emotion == "confused": |
| |
| 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: |
| 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: |
| |
| 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 |
| 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: |
| |
| |
| 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_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)) |
| |
| 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)) |
| |
| 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) |
| |
| 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: |
| |
| 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) |
| |
| draw.rectangle((hx - 38, hy - 12, hx + 38, hy + 6), |
| fill=(220, 80, 90), outline=(120, 30, 40), width=2) |
| |
| 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: |
| |
| 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) |
| |
| 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) |
| |
| 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) |
| |
| draw.line((cx - 36, cy - 14, cx + 36, cy - 14), fill=(200, 200, 220), width=3) |
| |
| 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 |
| |
| draw.rectangle((cx - 60, cy - 4, cx + 60, cy + 36), |
| fill=(220, 225, 230), outline=(160, 160, 170), width=2) |
| |
| draw.rectangle((cx - 56, cy + 8, cx + 56, cy + 32), fill=(100, 160, 200)) |
| |
| 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.""" |
| |
| lx, ly = lh |
| draw.rectangle((lx - 8, ly - 22, lx + 8, ly + 4), |
| fill=(60, 180, 200), outline=(20, 100, 120), width=2) |
| |
| draw.rectangle((lx + 6, ly - 20, lx + 16, ly - 12), |
| fill=(30, 120, 140), outline=(20, 80, 100), width=1) |
| |
| draw.line((lx + 8, ly - 10, lx + 8, ly + 4), |
| fill=(20, 100, 120), width=2) |
| |
| 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)) |
| |
| 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) |
| |
| 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 |
| |
| draw.line((hx, hy, hx + 6, hy - 18), |
| fill=(150, 100, 60), width=3) |
| |
| 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) |
| |
| 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) |
| |
| draw.ellipse((head_x - 30, head_y - 32, head_x + 30, head_y + 4), |
| fill=(230, 225, 205), outline=(180, 175, 150), width=2) |
| |
| 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) |
| |
| 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_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) |
| |
| 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) |
| |
| draw.rectangle((195, board_y - 10, 345, board_y), |
| fill=(180, 200, 230), outline=(120, 140, 170), width=1) |
| |
| 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) |
| |
| draw.rectangle((rx - 10, ry - 18, rx + 10, ry - 6), |
| fill=(40, 40, 50), outline=(160, 160, 180), width=2) |
| |
| 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 |
| |
| 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) |
| |
| 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) |
| |
| 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 |
| |
| draw.line((grip_x, grip_y - 30, grip_x, grip_y + 4), |
| fill=(160, 100, 60), width=4) |
| |
| 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) |
| |
| draw.rectangle((grip_x - 6, blade_top_y + 20, |
| grip_x + 6, blade_top_y + 36), |
| fill=(200, 40, 50)) |
| |
| 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) |
| |
| 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 |
| |
| draw.line((hx, hy, hx + 20, hy - 24), fill=(40, 40, 50), width=4) |
| |
| 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) |
| |
| 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 |
| |
| end_x = grip_x + 70 |
| end_y = grip_y - 100 |
| |
| draw.line((grip_x, grip_y, end_x, end_y), fill=(160, 110, 60), width=10) |
| |
| draw.ellipse((grip_x - 6, grip_y - 6, grip_x + 6, grip_y + 6), |
| fill=(120, 80, 40), outline=(60, 40, 20), width=2) |
| |
| 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 |
| |
| 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) |
| |
| 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_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) |
| |
| 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 |
| |
| 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) |
| |
| 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.""" |
| |
| cx = (lh[0] + rh[0]) / 2 |
| cy = (lh[1] + rh[1]) / 2 |
| |
| 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) |
| |
| 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)) |
| |
| 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_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)) |
| |
| 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: |
| |
| cx = (lh[0] + rh[0]) / 2 |
| cy = max(lh[1], rh[1]) + 20 |
| |
| draw.ellipse((cx - 40, cy, cx + 40, cy + 18), fill=(80, 50, 30)) |
| |
| 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_fn = SCENES.get(scene) |
| if scene_fn is not None: |
| scene_fn(draw) |
|
|
| |
| for a, b in SKELETON_EDGES: |
| draw.line((joints[a], joints[b]), fill=BONE_COLOR, width=BONE_WIDTH) |
|
|
| |
| _draw_ribs(draw, joints[NECK], joints[PELVIS]) |
| _draw_pelvis(draw, joints[PELVIS]) |
|
|
| |
| 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, |
| ) |
|
|
| |
| 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) |
|
|
| |
| 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]) |
|
|
| |
| _draw_skull(draw, joints[HEAD], emotion) |
|
|
| return img |
|
|
|
|
| |
| |
| |
|
|
| 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 |
|
|
|
|
| |
| |
| |
|
|
| 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 |
|
|
| |
| 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()) |
|
|