"""Extra emotion frames for the LED mask, in the same 46x58 display space + RGB style as :mod:`colorface` (black bg, cyan eyes, red mouth). These are the expression frames Gemini can trigger via ``set_expression`` that the base ``colorface.default_frames`` does not draw (heart, laugh, love-eyes, cool, sleepy, confused, kiss, star-struck). ``emotion_frames(...)`` returns ``{name: raw_bytes}`` ready for the mask's DIY image upload, exactly like ``colorface.default_frames``. Positions mirror ``colorface.build_face`` so the eyes/mouth line up with the rest of the set. """ from __future__ import annotations import math import colorface as _cf from colorface import DISPLAY_W as W, DISPLAY_H as H, encode from PIL import Image, ImageDraw # eye/mouth geometry copied from colorface.build_face so frames are consistent _EYE_L = W // 2 - 10 # 13 _EYE_R = W // 2 + 10 # 33 _EYE_T, _EYE_B = 15, 29 # normal eye top/bottom _EYE_W = 6 _MOUTH_CY = 44 _MOUTH_CX = W // 2 # 23 def _canvas(): img = Image.new("RGB", (W, H), (0, 0, 0)) return img, ImageDraw.Draw(img) def _round_eye(g, cx, eye_color, sclera_color, *, t=_EYE_T, b=_EYE_B, w=_EYE_W): g.ellipse([cx - w, t, cx + w, b], fill=sclera_color) g.ellipse([cx - w + 3, t + 4, cx + w - 3, b - 2], fill=eye_color) m = (t + b) // 2 g.ellipse([cx - 1, m - 1, cx + 1, m + 2], fill=(0, 0, 0)) def _happy_eye(g, cx, color): # upward "^"-ish squint (a smiling eye) g.arc([cx - 7, _EYE_T - 1, cx + 7, _EYE_B + 5], start=200, end=340, fill=color, width=3) def _heart(g, cx, cy, half, color): """A filled heart centred at (cx, cy), ``half`` = half-width.""" r = half / 2.0 g.pieslice([cx - half, cy - r, cx, cy + r], 0, 360, fill=color) # left lobe g.pieslice([cx, cy - r, cx + half, cy + r], 0, 360, fill=color) # right lobe g.polygon([(cx - half, cy + r * 0.2), (cx + half, cy + r * 0.2), (cx, cy + half)], fill=color) def _star(g, cx, cy, r, color): pts = [] for i in range(10): ang = -math.pi / 2 + i * math.pi / 5 rad = r if i % 2 == 0 else r * 0.45 pts.append((cx + rad * math.cos(ang), cy + rad * math.sin(ang))) g.polygon(pts, fill=color) def _smile(g, color, *, big=False): if big: # open grin g.chord([_MOUTH_CX - 13, _MOUTH_CY - 6, _MOUTH_CX + 13, _MOUTH_CY + 12], start=0, end=180, fill=color) else: g.arc([_MOUTH_CX - 12, _MOUTH_CY - 8, _MOUTH_CX + 12, _MOUTH_CY + 8], start=20, end=160, fill=color, width=4) # Fixed emoji colors — these frames are icons, not part of the face's colour # scheme, so a heart is always red and a thumb always yellow regardless of the # user's chosen eye/mouth colours. _RED = (255, 45, 75) _PINK = (255, 95, 155) _YELLOW = (255, 200, 40) # -- individual emotion drawings --------------------------------------------- def _heart_face(eye, mouth, sclera): img, g = _canvas() _heart(g, W // 2, 26, 18, _RED) # one big RED heart fills the face return img def _laugh(eye, mouth, sclera): img, g = _canvas() _happy_eye(g, _EYE_L, eye) _happy_eye(g, _EYE_R, eye) _smile(g, mouth, big=True) # wide open grin # a joy tear under each eye for cx in (_EYE_L, _EYE_R): g.ellipse([cx - 2, _EYE_B + 3, cx + 2, _EYE_B + 9], fill=(0, 180, 255)) return img def _love(eye, mouth, sclera): img, g = _canvas() _heart(g, _EYE_L, 22, 8, _PINK) # pink heart-shaped eyes _heart(g, _EYE_R, 22, 8, _PINK) _smile(g, _PINK) return img def _cool(eye, mouth, sclera): img, g = _canvas() frame = (40, 40, 55) lens = (10, 10, 20) # two lenses + bridge (sunglasses) for cx in (_EYE_L, _EYE_R): g.rounded_rectangle([cx - 8, _EYE_T, cx + 8, _EYE_B + 1], radius=4, fill=lens, outline=frame, width=2) g.line([cx - 5, _EYE_T + 3, cx + 2, _EYE_T + 3], fill=eye, width=2) # glint g.line([_EYE_L + 8, _EYE_T + 3, _EYE_R - 8, _EYE_T + 3], fill=frame, width=3) # a cool little smirk (raised on one side) g.arc([_MOUTH_CX - 11, _MOUTH_CY - 5, _MOUTH_CX + 12, _MOUTH_CY + 8], start=15, end=120, fill=mouth, width=4) return img def _sleepy(eye, mouth, sclera): img, g = _canvas() # droopy half-closed eyes: lid arc over a thin slit for cx in (_EYE_L, _EYE_R): g.arc([cx - 7, _EYE_T + 2, cx + 7, _EYE_B + 4], start=160, end=20, fill=eye, width=3) # small tired mouth g.ellipse([_MOUTH_CX - 4, _MOUTH_CY - 2, _MOUTH_CX + 4, _MOUTH_CY + 4], fill=mouth) # zZ drawn as cheap line-glyphs (no font dependency) for (x, y, s) in ((36, 8, 5), (41, 3, 3)): g.line([x, y, x + s, y], fill=eye, width=1) g.line([x + s, y, x, y + s], fill=eye, width=1) g.line([x, y + s, x + s, y + s], fill=eye, width=1) return img def _confused(eye, mouth, sclera): img, g = _canvas() _round_eye(g, _EYE_L, eye, sclera) # normal eye _round_eye(g, _EYE_R, eye, sclera, t=_EYE_T - 3, b=_EYE_B - 3, w=5) # raised/small # raised brow over the small eye g.line([_EYE_R - 6, _EYE_T - 6, _EYE_R + 6, _EYE_T - 9], fill=eye, width=2) # wavy/squiggle mouth pts = [(_MOUTH_CX - 12 + i * 4, _MOUTH_CY + (3 if i % 2 else -3)) for i in range(7)] g.line(pts, fill=mouth, width=3, joint="curve") return img def _kiss(eye, mouth, sclera): img, g = _canvas() _round_eye(g, _EYE_L, eye, sclera) g.line([_EYE_R - 6, (_EYE_T + _EYE_B) // 2, _EYE_R + 6, (_EYE_T + _EYE_B) // 2], fill=eye, width=3) # winking eye # puckered red lips g.ellipse([_MOUTH_CX - 4, _MOUTH_CY - 4, _MOUTH_CX + 4, _MOUTH_CY + 5], fill=_RED) g.ellipse([_MOUTH_CX - 2, _MOUTH_CY - 2, _MOUTH_CX + 2, _MOUTH_CY + 3], fill=(0, 0, 0)) _heart(g, 37, 13, 6, _RED) # little floating red heart return img def _star_struck(eye, mouth, sclera): img, g = _canvas() _star(g, _EYE_L, 22, 7, (255, 220, 0)) _star(g, _EYE_R, 22, 7, (255, 220, 0)) _smile(g, mouth, big=True) return img def _thumbs_up(eye, mouth, sclera): # a 👍: one bold vertical thumb + a bold fist block, kept simple so it reads # on the low-res LED grid (fine detail just blurs into a blob). img, g = _canvas() g.rounded_rectangle([11, 30, 37, 52], radius=8, fill=_YELLOW) # fist block g.rounded_rectangle([13, 6, 29, 34], radius=8, fill=_YELLOW) # big thumb up g.line([30, 34, 36, 34], fill=(0, 0, 0), width=3) # thumb/finger split return img _BUILDERS = { "heart": _heart_face, "laugh": _laugh, "love": _love, "cool": _cool, "sleepy": _sleepy, "confused": _confused, "kiss": _kiss, "star_struck": _star_struck, "thumbs_up": _thumbs_up, } def emotion_frames(*, eye_color=_cf.DEFAULT_EYE, mouth_color=_cf.DEFAULT_MOUTH, sclera_color=_cf.WHITE, include=None) -> dict: """Return ``{name: raw_bytes}`` for the extra emotion frames. ``include`` optionally restricts to a subset (a set/list of names) so the caller can honour the mask's slot budget. """ names = list(_BUILDERS) if include is None else [n for n in _BUILDERS if n in include] out = {} for name in names: img = _BUILDERS[name](eye_color, mouth_color, sclera_color) out[name] = encode(img) return out