Sanad_Package_2/vendor/Sanad/face/emotion_frames.py

209 lines
7.3 KiB
Python

"""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