#!/usr/bin/env python3 """ Genera 12 GIFs animados de 80x80 con caritas kawaii, una por emoción. Estilo minimalista: cara blanca, ojos negros, cejas, boca, blush rosado. Se guardan en firmware/gifs/.gif y se suben a SPIFFS del ESP32. """ import os from PIL import Image, ImageDraw SIZE = 80 OUT_DIR = os.path.join(os.path.dirname(__file__), "gifs") os.makedirs(OUT_DIR, exist_ok=True) # Paleta kawaii SKIN = (255, 230, 200, 255) # piel WHITE = (255, 255, 255, 255) BLACK = (20, 20, 30, 255) PINK = (255, 150, 180, 255) # blush LBLUE = (180, 215, 255, 255) # lágrima RED = (220, 70, 90, 255) GREEN = (110, 200, 130, 255) YELLOW = (250, 200, 60, 255) PURPLE = (180, 130, 230, 255) def new_canvas(): return Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0)) def draw_face(draw, *, eye_y=40, eye_lx=24, eye_rx=56, eye_w=12, eye_h=14, pupil_dx=0, pupil_dy=0, brow_l=(20, 20, 32, 20), brow_r=(48, 20, 60, 20), mouth_kind="neutral", open_pct=100, blush=True, extras=None): """Dibuja la cara con los parámetros dados. Devuelve la imagen.""" # Borde de la cara (círculo) draw.ellipse([(6, 6), (SIZE - 6, SIZE - 6)], fill=SKIN, outline=BLACK, width=2) # Blush if blush: draw.ellipse([(10, 48), (24, 56)], fill=PINK) draw.ellipse([(56, 48), (70, 56)], fill=PINK) # Cejas draw.line([(brow_l[0], brow_l[1]), (brow_l[2], brow_l[3])], fill=BLACK, width=3) draw.line([(brow_r[0], brow_r[1]), (brow_r[2], brow_r[3])], fill=BLACK, width=3) # Ojos open_h = max(2, int(eye_h * open_pct / 100)) # izquierdo draw.ellipse([(eye_lx - eye_w // 2, eye_y - open_h // 2 + pupil_dy), (eye_lx + eye_w // 2, eye_y + open_h // 2 + pupil_dy)], fill=WHITE, outline=BLACK, width=2) # derecho draw.ellipse([(eye_rx - eye_w // 2, eye_y - open_h // 2 + pupil_dy), (eye_rx + eye_w // 2, eye_y + open_h // 2 + pupil_dy)], fill=WHITE, outline=BLACK, width=2) # pupilas (solo si el ojo está abierto) if open_pct > 30: pdx = 3 if pupil_dx > 0 else (-3 if pupil_dx < 0 else 0) pdy = 3 if pupil_dy > 0 else (-3 if pupil_dy < 0 else 0) draw.ellipse([(eye_lx - 2 + pdx, eye_y - 2 + pdy), (eye_lx + 2 + pdx, eye_y + 2 + pdy)], fill=BLACK) draw.ellipse([(eye_rx - 2 + pdx, eye_y - 2 + pdy), (eye_rx + 2 + pdx, eye_y + 2 + pdy)], fill=BLACK) # Boca if mouth_kind == "neutral": draw.line([(34, 60), (46, 60)], fill=BLACK, width=2) elif mouth_kind == "happy": draw.arc([(28, 54), (52, 70)], 10, 170, fill=BLACK, width=3) elif mouth_kind == "open_smile": draw.ellipse([(30, 54), (50, 70)], fill=RED, outline=BLACK, width=2) elif mouth_kind == "sad": draw.arc([(28, 60), (52, 76)], 190, 350, fill=BLACK, width=3) elif mouth_kind == "frown": draw.line([(30, 70), (50, 60)], fill=BLACK, width=2) elif mouth_kind == "kiss": draw.ellipse([(34, 60), (46, 68)], fill=RED, outline=BLACK, width=1) elif mouth_kind == "diamond": # boca de sorpresa draw.polygon([(40, 54), (48, 62), (40, 70), (32, 62)], fill=RED, outline=BLACK) elif mouth_kind == "teeth": # boca apretada draw.rectangle([(30, 60), (50, 65)], fill=WHITE, outline=BLACK, width=1) for x in range(34, 50, 4): draw.line([(x, 60), (x, 65)], fill=BLACK) elif mouth_kind == "tongue": draw.ellipse([(30, 56), (50, 68)], fill=RED, outline=BLACK, width=2) draw.ellipse([(36, 62), (44, 70)], fill=PINK, outline=BLACK, width=1) elif mouth_kind == "zigzag": # sonrisa traviesa for x in range(30, 52, 4): draw.line([(x, 60), (x + 3, 67)], fill=BLACK, width=2) for x in range(30, 52, 4): draw.line([(x + 3, 67), (x + 6, 60)], fill=BLACK, width=2) if extras: extras(draw) def save_anim(frames, filename, duration=80, loop=0): """Guarda una secuencia de frames como GIF animado.""" # Convertir a 'P' mode con paleta transparente out = [] for f in frames: # Asegurar RGBA out.append(f.convert("RGBA")) out_path = os.path.join(OUT_DIR, filename) out[0].save(out_path, save_all=True, append_images=out[1:], duration=duration, loop=loop, transparency=0, disposal=2, optimize=True) print(f" -> {filename}: {os.path.getsize(out_path)} bytes, {len(out)} frames") # ─────────────────── 1. NEUTRAL ─────────────────── def gen_neutral(): print("Generando 01_neutral.gif") frames = [] for i in range(8): img = new_canvas() d = ImageDraw.Draw(img) pupil_dx = (1 if i % 2 == 0 else -1) # mirada lateral draw_face(d, eye_y=40, pupil_dx=pupil_dx, mouth_kind="neutral", brow_l=(28, 18, 32, 18), brow_r=(48, 18, 52, 18), open_pct=100, blush=True) frames.append(img) save_anim(frames, "01_neutral.gif", duration=120) # ─────────────────── 2. HAPPY ─────────────────── def gen_happy(): print("Generando 02_happy.gif") frames = [] for i in range(10): # bounce up/down offset = -1 if i % 2 == 0 else 1 img = new_canvas() d = ImageDraw.Draw(img) d.ellipse([(6, 6 + offset), (SIZE - 6, SIZE - 6 + offset)], fill=SKIN, outline=BLACK, width=2) # blush d.ellipse([(10, 48 + offset), (24, 56 + offset)], fill=PINK) d.ellipse([(56, 48 + offset), (70, 56 + offset)], fill=PINK) # ojos felices (arcos) d.arc([(18, 36 + offset), (32, 50 + offset)], 200, 340, fill=BLACK, width=3) d.arc([(48, 36 + offset), (62, 50 + offset)], 200, 340, fill=BLACK, width=3) # boca sonrisa abierta d.arc([(28, 54 + offset), (52, 72 + offset)], 10, 170, fill=BLACK, width=3) frames.append(img) save_anim(frames, "02_happy.gif", duration=80) # ─────────────────── 3. WORRIED ─────────────────── def gen_worried(): print("Generando 03_worried.gif") frames = [] for i in range(10): img = new_canvas() d = ImageDraw.Draw(img) # cejas en pico draw_face(d, eye_y=42, mouth_kind="happy", brow_l=(18, 16, 32, 22), brow_r=(48, 22, 62, 16), open_pct=85, blush=False) # gota de sudor if i % 3 == 0: d.polygon([(62, 12), (66, 18), (62, 24)], fill=LBLUE, outline=BLACK) frames.append(img) save_anim(frames, "03_worried.gif", duration=120) # ─────────────────── 4. SAD ─────────────────── def gen_sad(): print("Generando 04_sad.gif") frames = [] for i in range(8): img = new_canvas() d = ImageDraw.Draw(img) offset = 0 if i < 4 else 2 draw_face(d, eye_y=42, mouth_kind="sad", brow_l=(18, 20, 32, 14), brow_r=(48, 14, 62, 20), open_pct=70, blush=False) # lágrima cayendo ty = 30 + offset d.ellipse([(14, ty), (22, ty + 8)], fill=LBLUE, outline=BLACK, width=1) frames.append(img) save_anim(frames, "04_sad.gif", duration=150) # ─────────────────── 5. CRY ─────────────────── def gen_cry(): print("Generando 05_cry.gif") frames = [] for i in range(10): img = new_canvas() d = ImageDraw.Draw(img) # ojos cerrados llorando d.ellipse([(6, 6), (SIZE - 6, SIZE - 6)], fill=SKIN, outline=BLACK, width=2) # cejas tristes d.line([(18, 18), (32, 24)], fill=BLACK, width=3) d.line([(48, 24), (62, 18)], fill=BLACK, width=3) # ojos cerrados (arcos hacia abajo) d.arc([(18, 38), (32, 50)], 20, 160, fill=BLACK, width=3) d.arc([(48, 38), (62, 50)], 20, 160, fill=BLACK, width=3) # boca muy triste d.arc([(28, 60), (52, 76)], 190, 350, fill=BLACK, width=3) # lágrimas cayendo t1y = 30 + (i * 3) % 30 t2y = 35 + (i * 4) % 25 d.ellipse([(20, t1y), (24, t1y + 5)], fill=LBLUE, outline=BLACK, width=1) d.ellipse([(56, t2y), (60, t2y + 5)], fill=LBLUE, outline=BLACK, width=1) frames.append(img) save_anim(frames, "05_cry.gif", duration=100) # ─────────────────── 6. SURPRISED ─────────────────── def gen_surprised(): print("Generando 06_surprised.gif") frames = [] for i in range(8): img = new_canvas() d = ImageDraw.Draw(img) draw_face(d, eye_y=40, mouth_kind="diamond", brow_l=(28, 8, 32, 8), brow_r=(48, 8, 52, 8), open_pct=100, blush=False) # vibración if i % 2 == 0: d.line([(0, 0), (10, 0)], fill=BLACK) d.line([(0, 4), (10, 4)], fill=BLACK) d.line([(70, 0), (80, 0)], fill=BLACK) d.line([(70, 4), (80, 4)], fill=BLACK) frames.append(img) save_anim(frames, "06_surprised.gif", duration=100) # ─────────────────── 7. ANGRY ─────────────────── def gen_angry(): print("Generando 07_angry.gif") frames = [] for i in range(10): img = new_canvas() d = ImageDraw.Draw(img) # temblor ox = (1 if i % 2 == 0 else -1) oy = (1 if i % 3 == 0 else 0) d.ellipse([(6 + ox, 6 + oy), (SIZE - 6 + ox, SIZE - 6 + oy)], fill=SKIN, outline=BLACK, width=2) # blush rojo d.ellipse([(10, 48), (24, 56)], fill=RED) d.ellipse([(56, 48), (70, 56)], fill=RED) # cejas en pico invertido d.line([(18, 14), (32, 24)], fill=BLACK, width=3) d.line([(48, 24), (62, 14)], fill=BLACK, width=3) # ojos entrecerrados d.ellipse([(18, 38), (30, 44)], fill=WHITE, outline=BLACK, width=2) d.ellipse([(50, 38), (62, 44)], fill=WHITE, outline=BLACK, width=2) d.ellipse([(22, 40), (26, 42)], fill=BLACK) d.ellipse([(54, 40), (58, 42)], fill=BLACK) # boca fruncida d.line([(32, 64), (48, 64)], fill=BLACK, width=3) # vena de enfado if i % 2 == 0: d.line([(64, 14), (74, 24)], fill=RED, width=2) d.line([(64, 14), (74, 14)], fill=RED, width=2) frames.append(img) save_anim(frames, "07_angry.gif", duration=80) # ─────────────────── 8. SLEEPY ─────────────────── def gen_sleepy(): print("Generando 08_sleepy.gif") frames = [] for i in range(12): img = new_canvas() d = ImageDraw.Draw(img) # cabeceo offset = 0 if i < 6 else 4 d.ellipse([(6, 6 + offset), (SIZE - 6, SIZE - 6 + offset)], fill=SKIN, outline=BLACK, width=2) # cejas relajadas d.line([(22, 22), (28, 20)], fill=BLACK, width=2) d.line([(52, 20), (58, 22)], fill=BLACK, width=2) # ojos cerrados (líneas horizontales) d.line([(18, 42), (30, 42)], fill=BLACK, width=3) d.line([(50, 42), (62, 42)], fill=BLACK, width=3) # ZZZ if i >= 4: d.text((66, 8 - offset // 2), "z", fill=BLACK) d.text((72, 4 - offset // 2), "z", fill=BLACK) # boca neutral d.line([(36, 60), (44, 60)], fill=BLACK, width=2) frames.append(img) save_anim(frames, "08_sleepy.gif", duration=150) # ─────────────────── 9. WINK ─────────────────── def gen_wink(): print("Generando 09_wink.gif") frames = [] for i in range(10): img = new_canvas() d = ImageDraw.Draw(img) d.ellipse([(6, 6), (SIZE - 6, SIZE - 6)], fill=SKIN, outline=BLACK, width=2) d.ellipse([(10, 48), (24, 56)], fill=PINK) d.ellipse([(56, 48), (70, 56)], fill=PINK) # ceja izquierda arqueada d.line([(18, 18), (32, 14)], fill=BLACK, width=3) d.line([(48, 16), (62, 22)], fill=BLACK, width=3) # ojo izquierdo abierto, derecho cerrado (parpadeo) if i < 5: d.ellipse([(18, 36), (30, 50)], fill=WHITE, outline=BLACK, width=2) d.ellipse([(22, 41), (26, 45)], fill=BLACK) d.line([(48, 43), (62, 43)], fill=BLACK, width=3) # cerrado else: d.line([(18, 43), (30, 43)], fill=BLACK, width=3) # cerrado d.ellipse([(50, 36), (62, 50)], fill=WHITE, outline=BLACK, width=2) d.ellipse([(54, 41), (58, 45)], fill=BLACK) # sonrisa d.arc([(28, 54), (52, 72)], 10, 170, fill=BLACK, width=3) # destello if i == 2: d.line([(60, 28), (66, 34)], fill=YELLOW, width=2) frames.append(img) save_anim(frames, "09_wink.gif", duration=120) # ─────────────────── 10. LOVE ─────────────────── def gen_love(): print("Generando 10_love.gif") frames = [] for i in range(10): img = new_canvas() d = ImageDraw.Draw(img) d.ellipse([(6, 6), (SIZE - 6, SIZE - 6)], fill=SKIN, outline=BLACK, width=2) d.ellipse([(10, 48), (24, 56)], fill=PINK) d.ellipse([(56, 48), (70, 56)], fill=PINK) # ojos en forma de corazón for cx in (24, 56): # dos círculos + triángulo para hacer corazón pixel art d.ellipse([(cx - 6, 36), (cx, 42)], fill=RED) d.ellipse([(cx, 36), (cx + 6, 42)], fill=RED) d.polygon([(cx - 6, 41), (cx + 6, 41), (cx, 50)], fill=RED) # sonrisa d.arc([(28, 54), (52, 72)], 10, 170, fill=BLACK, width=3) # corazón flotante if i % 2 == 0: hx = 56 + (i // 2) hy = 24 - (i // 2) d.ellipse([(hx - 4, hy - 2), (hx, hy + 2)], fill=RED) d.ellipse([(hx, hy - 2), (hx + 4, hy + 2)], fill=RED) d.polygon([(hx - 4, hy + 1), (hx + 4, hy + 1), (hx, hy + 8)], fill=RED) frames.append(img) save_anim(frames, "10_love.gif", duration=100) # ─────────────────── 11. WORKING_HARD ─────────────────── def gen_working(): print("Generando 11_working.gif") frames = [] for i in range(8): img = new_canvas() d = ImageDraw.Draw(img) # cejas concentradas d.ellipse([(6, 6), (SIZE - 6, SIZE - 6)], fill=SKIN, outline=BLACK, width=2) d.ellipse([(10, 48), (24, 56)], fill=PINK) d.ellipse([(56, 48), (70, 56)], fill=PINK) d.line([(18, 14), (32, 22)], fill=BLACK, width=3) d.line([(48, 22), (62, 14)], fill=BLACK, width=3) # ojos con mirada fija al frente d.ellipse([(18, 38), (30, 50)], fill=WHITE, outline=BLACK, width=2) d.ellipse([(50, 38), (62, 50)], fill=WHITE, outline=BLACK, width=2) d.ellipse([(22, 42), (26, 46)], fill=BLACK) d.ellipse([(54, 42), (58, 46)], fill=BLACK) # boca apretada d.rectangle([(32, 60), (48, 64)], fill=WHITE, outline=BLACK, width=1) for x in range(34, 48, 3): d.line([(x, 60), (x, 64)], fill=BLACK) # gotas de sudor cayendo sy = 30 + (i * 5) % 30 d.polygon([(64, sy - 4), (68, sy), (64, sy + 4)], fill=LBLUE, outline=BLACK, width=1) frames.append(img) save_anim(frames, "11_working.gif", duration=100) # ─────────────────── 12. EXCITED ─────────────────── def gen_excited(): print("Generando 12_excited.gif") frames = [] for i in range(10): # bounce fuerte offset = -3 if i % 2 == 0 else 3 img = new_canvas() d = ImageDraw.Draw(img) d.ellipse([(6, 6 + offset), (SIZE - 6, SIZE - 6 + offset)], fill=SKIN, outline=BLACK, width=2) d.ellipse([(10, 48 + offset), (24, 56 + offset)], fill=PINK) d.ellipse([(56, 48 + offset), (70, 56 + offset)], fill=PINK) # ojos grandes abiertos d.ellipse([(16, 36 + offset), (32, 52 + offset)], fill=WHITE, outline=BLACK, width=2) d.ellipse([(48, 36 + offset), (64, 52 + offset)], fill=WHITE, outline=BLACK, width=2) # pupilas bailando pdx = -2 if i % 2 == 0 else 2 d.ellipse([(22 + pdx, 42 + offset), (26 + pdx, 46 + offset)], fill=BLACK) d.ellipse([(54 + pdx, 42 + offset), (58 + pdx, 46 + offset)], fill=BLACK) # boca enorme d.ellipse([(28, 54 + offset), (52, 70 + offset)], fill=RED, outline=BLACK, width=2) # lengua d.ellipse([(34, 62 + offset), (46, 70 + offset)], fill=PINK) # destellos if i % 2 == 0: d.line([(8, 8), (16, 16)], fill=YELLOW, width=2) d.line([(64, 8), (72, 16)], fill=YELLOW, width=2) frames.append(img) save_anim(frames, "12_excited.gif", duration=70) # ─────────────────── MAIN ─────────────────── if __name__ == "__main__": print(f"Generando 12 GIFs en {OUT_DIR}/") gen_neutral() gen_happy() gen_worried() gen_sad() gen_cry() gen_surprised() gen_angry() gen_sleepy() gen_wink() gen_love() gen_working() gen_excited() print(f"\nListo. {os.listdir(OUT_DIR)}") # Tamaño total total = sum(os.path.getsize(os.path.join(OUT_DIR, f)) for f in os.listdir(OUT_DIR)) print(f"Tamaño total: {total} bytes ({total / 1024:.1f} KB)")