dummybot-firmware/gen_gifs.py

438 lines
18 KiB
Python

#!/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/<filename>.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)")