#!/usr/bin/env python3 """Genera el avatar de @chemavx_researchowl_bot: búho con gafas de lectura. Mismo método que los avatares de hermes-bot y roswell-corpus: se dibuja a 4× y se reduce, que es la forma barata de tener bordes suaves sin antialiasing propio en PIL. Telegram recorta en CÍRCULO, así que todo lo que importa vive dentro del círculo inscrito. Por qué gafas y no un búho a secas: el búho solo dice "búho". El aro sobre cada ojo, con su puente, se lee como gafas de leer al tamaño grande y como disco facial al pequeño, y ninguna de las dos lecturas es errónea. Es el único guiño a la investigación que sobrevive a 48 px; una lupa o un libro se convierten en una mancha. python3 make_avatar.py # deja el PNG en assets/avatar.png """ import math import os from PIL import Image, ImageDraw, ImageFilter SIZE = 640 SS = 4 # supersampling W = SIZE * SS CX = CY = W // 2 AMBAR = (236, 173, 84) # plumaje AMBAR_OSC = (184, 116, 52) # alas MONTURA = (46, 26, 30) # gafas y pico: tienen que contrastar con el plumaje CREMA = (255, 246, 230) # ojos TINTA = (30, 16, 36) # pupilas y huecos AQUI = os.path.dirname(os.path.abspath(__file__)) def fondo() -> Image.Image: """Degradado radial ciruela, dibujado pequeño y ampliado.""" n = 128 g = Image.new("RGB", (n, n)) px = g.load() for y in range(n): for x in range(n): d = min(1.0, math.hypot(x - n / 2, y - n / 2) / (n / 2 * 1.10)) px[x, y] = (int(56 + (11 - 56) * d), int(33 + (6 - 33) * d), int(66 + (16 - 66) * d)) return g.resize((W, W), Image.BICUBIC).convert("RGBA") def tinta(mascara: Image.Image, color) -> Image.Image: """Convierte una máscara L en una capa RGBA del color dado.""" capa = Image.new("RGBA", mascara.size, color + (0,)) capa.putalpha(mascara) return capa def caja(x0, y0, x1, y1): """Rectángulo en fracciones de W → píxeles.""" return [x0 * W, y0 * W, x1 * W, y1 * W] def cuerpo() -> Image.Image: """Silueta completa: penachos + cuerpo. Se reutiliza como máscara.""" m = Image.new("L", (W, W), 0) d = ImageDraw.Draw(m) for signo in (-1, 1): d.polygon([ (CX + signo * 0.262 * W, 0.352 * W), (CX + signo * 0.108 * W, 0.300 * W), (CX + signo * 0.196 * W, 0.148 * W), ], fill=255) d.ellipse(caja(0.20, 0.28, 0.80, 0.84), fill=255) return m def alas(mascara: Image.Image) -> Image.Image: """Dos alas plegadas a los lados, recortadas contra la silueta. Van recortadas y no dibujadas a pelo porque el ala tiene que morir justo en el borde del cuerpo: si asoma, el búho deja de tener contorno limpio y a tamaño pequeño se convierte en un borrón con orejas. """ capa = Image.new("RGBA", (W, W), (0, 0, 0, 0)) d = ImageDraw.Draw(capa) for signo in (-1, 1): cx = CX + signo * 0.205 * W d.ellipse([cx - 0.115 * W, 0.455 * W, cx + 0.115 * W, 0.855 * W], fill=AMBAR_OSC + (255,)) return Image.composite(capa, Image.new("RGBA", (W, W), (0, 0, 0, 0)), mascara) def cara() -> Image.Image: """Ojos, gafas y pico.""" capa = Image.new("RGBA", (W, W), (0, 0, 0, 0)) d = ImageDraw.Draw(capa) y = 0.425 r = 0.105 grosor = int(W * 0.019) for signo in (-1, 1): cx = CX + signo * 0.125 * W d.ellipse([cx - r * W, (y - r) * W, cx + r * W, (y + r) * W], fill=CREMA + (255,)) d.ellipse([cx - r * W, (y - r) * W, cx + r * W, (y + r) * W], outline=MONTURA + (255,), width=grosor) pr = 0.046 * W d.ellipse([cx - pr, y * W - pr, cx + pr, y * W + pr], fill=TINTA + (255,)) br = 0.017 * W # reflejo: sin él la mirada es de muñeco bx, by = cx - 0.020 * W, y * W - 0.030 * W d.ellipse([bx - br, by - br, bx + br, by + br], fill=(255, 255, 255, 210)) # puente de las gafas d.rectangle([CX - 0.022 * W, y * W - grosor / 2, CX + 0.022 * W, y * W + grosor / 2], fill=MONTURA + (255,)) d.polygon([(CX - 0.042 * W, 0.500 * W), (CX + 0.042 * W, 0.500 * W), (CX, 0.575 * W)], fill=MONTURA + (255,)) return capa def main(): img = fondo() silueta = cuerpo() # resplandor: la silueta desenfocada por debajo, para despegar al búho del # fondo cuando el icono se ve pequeño brillo = tinta(silueta.filter(ImageFilter.GaussianBlur(W * 0.030)), AMBAR) img.alpha_composite(Image.blend(Image.new("RGBA", (W, W), (0, 0, 0, 0)), brillo, 0.5)) img.alpha_composite(tinta(silueta, AMBAR)) img.alpha_composite(alas(silueta)) img.alpha_composite(cara()) # aro fino: le da borde al icono cuando Telegram lo recorta en círculo m = int(W * 0.045) ImageDraw.Draw(img).ellipse([m, m, W - m, W - m], outline=AMBAR + (110,), width=int(W * 0.008)) salida = os.path.join(AQUI, "avatar.png") img.convert("RGB").resize((SIZE, SIZE), Image.LANCZOS).save(salida, "PNG") print(salida) if __name__ == "__main__": main()