#!/usr/bin/env python3 """生成 EzVibe 桌宠像素艺术图片(5 种情绪 × 2 帧)。""" from PIL import Image import os, math # ── 画布参数 ──────────────────────────────────────────────────────────────── W, H = 64, 64 CX, CY, R = 32, 34, 22 # ── 调色板 ────────────────────────────────────────────────────────────────── P = { "bg": (0, 0, 0, 0), "white": (255, 255, 255, 255), "dark": (40, 40, 60, 255), "pink": (255, 150, 180, 255), "yellow": (255, 220, 100, 255), "green": (80, 200, 120, 255), "blue": (80, 160, 255, 255), "red": (255, 80, 80, 255), "black": (20, 20, 30, 255), "orange": (255, 160, 60, 255), "mouth": (255, 80, 80, 255), "blush": (255, 120, 160, 180), "blush_r": (255, 80, 80, 150), "blush_y": (255, 120, 120, 150), } def px(x, y, key): if 0 <= x < W and 0 <= y < H: pixels[y, x] = P.get(key, key) def fill(x, y, w, h, key): for dy in range(h): for dx in range(w): px(x+dx, y+dy, key) def circle(cx, cy, r, key): for angle in range(360): a = math.radians(angle) px(int(cx + r*math.cos(a)), int(cy + r*math.sin(a)), key) def fill_circle(cx, cy, r, key): for dy in range(-r, r+1): for dx in range(-r, r+1): if dx*dx + dy*dy <= r*r: px(cx+dx, cy+dy, key) def line(x1, y1, x2, y2, key): dx, dy = abs(x2-x1), abs(y2-y1) sx = 1 if x1 < x2 else -1 sy = 1 if y1 < y2 else -1 err = dx - dy while True: px(x1, y1, key) if x1 == x2 and y1 == y2: break e2 = 2*err if e2 > -dy: err -= dy; x1 += sx if e2 < dx: err += dx; y1 += sy # ── 基础猫脸 ──────────────────────────────────────────────────────────────── def cat_base(): fill_circle(CX, CY, R, "white") fill_circle(CX-R+1, CY-R-2, 5, "pink") fill_circle(CX+R-1, CY-R-2, 5, "pink") # 耳朵轮廓 line(CX-R+4, CY-R, CX-R-3, CY-R-12, "dark") line(CX-R-3, CY-R-12, CX-R+10, CY-R+1, "dark") line(CX+R-4, CY-R, CX+R+3, CY-R-12, "dark") line(CX+R+3, CY-R-12, CX+R-10, CY-R+1, "dark") circle(CX, CY, R, "dark") fill(CX-10, CY+R-4, 20, 8, "white") # ── 帧绘制函数 ───────────────────────────────────────────────────────────── def idle_0(): """待机帧1:正常眨眼。""" cat_base() fill_circle(CX-9, CY-4, 5, "green") fill_circle(CX-10, CY-5, 2, "white") fill_circle(CX-7, CY-3, 1, "black") fill_circle(CX+9, CY-4, 5, "green") fill_circle(CX+8, CY-5, 2, "white") fill_circle(CX+11, CY-3, 1, "black") fill(CX-2, CY+2, 4, 3, "pink") line(CX-2, CY+5, CX-6, CY+8, "dark") line(CX+2, CY+5, CX+6, CY+8, "dark") fill_circle(CX-15, CY+2, 3, "blush") fill_circle(CX+15, CY+2, 3, "blush") line(CX-13, CY, CX-22, CY-2, "dark") line(CX-13, CY+2, CX-22, CY+2, "dark") line(CX+13, CY, CX+22, CY-2, "dark") line(CX+13, CY+2, CX+22, CY+2, "dark") def idle_1(): """待机帧2:闭眼。""" cat_base() fill(CX-13, CY-4, 9, 2, "dark") fill(CX+4, CY-4, 9, 2, "dark") fill(CX-2, CY+2, 4, 3, "pink") line(CX-2, CY+5, CX-6, CY+8, "dark") line(CX+2, CY+5, CX+6, CY+8, "dark") fill_circle(CX-15, CY+2, 3, "blush") fill_circle(CX+15, CY+2, 3, "blush") line(CX-13, CY, CX-22, CY-2, "dark") line(CX-13, CY+2, CX-22, CY+2, "dark") line(CX+13, CY, CX+22, CY-2, "dark") line(CX+13, CY+2, CX+22, CY+2, "dark") def happy_0(): """开心帧1:眯眼大笑。""" cat_base() line(CX-13, CY-1, CX-6, CY-4, "dark") line(CX-13, CY-4, CX-6, CY-1, "dark") line(CX+6, CY-4, CX+13, CY-1, "dark") line(CX+6, CY-1, CX+13, CY-4, "dark") fill_circle(CX-15, CY+1, 4, "blush") fill_circle(CX+15, CY+1, 4, "blush") fill(CX-7, CY+4, 14, 6, "mouth") line(CX-7, CY+4, CX+7, CY+4, "dark") line(CX-7, CY+10, CX+7, CY+10, "dark") fill(CX-3, CY+7, 6, 4, "pink") def happy_1(): """开心帧2:星星眼。""" cat_base() for a in range(0, 360, 30): rad = math.radians(a) px(int(CX-9 + 5*math.cos(rad)), int(CY-4 + 5*math.sin(rad)), "yellow") fill_circle(CX-9, CY-4, 2, "yellow") for a in range(0, 360, 30): rad = math.radians(a) px(int(CX+9 + 5*math.cos(rad)), int(CY-4 + 5*math.sin(rad)), "yellow") fill_circle(CX+9, CY-4, 2, "yellow") fill_circle(CX-15, CY+1, 4, "blush") fill_circle(CX+15, CY+1, 4, "blush") fill(CX-6, CY+4, 12, 5, "mouth") line(CX-6, CY+4, CX+6, CY+4, "dark") fill(CX-2, CY+6, 4, 3, "pink") def focused_0(): """专注帧1:眯眼盯视。""" cat_base() fill(CX-12, CY-5, 10, 4, "blue") fill(CX+2, CY-5, 10, 4, "blue") fill(CX-13, CY-6, 11, 1, "dark") fill(CX-13, CY-2, 11, 1, "dark") fill(CX+2, CY-6, 11, 1, "dark") fill(CX+2, CY-2, 11, 1, "dark") fill(CX-10, CY-3, 1, 1, "white") fill(CX+5, CY-3, 1, 1, "white") fill(CX-12, CY-10, 9, 2, "dark") fill(CX+3, CY-10, 9, 2, "dark") fill(CX-2, CY+2, 4, 3, "pink") fill(CX-5, CY+5, 10, 2, "dark") line(CX-13, CY, CX-22, CY-4, "dark") line(CX-13, CY+2, CX-22, CY, "dark") line(CX-13, CY+4, CX-22, CY+4, "dark") line(CX+13, CY, CX+22, CY-4, "dark") line(CX+13, CY+2, CX+22, CY, "dark") line(CX+13, CY+4, CX+22, CY+4, "dark") def focused_1(): """专注帧2:眨眼。""" cat_base() fill(CX-12, CY-4, 10, 5, "blue") fill(CX+2, CY-4, 10, 5, "blue") fill(CX-10, CY-2, 1, 1, "white") fill(CX+4, CY-2, 1, 1, "white") fill(CX-12, CY-10, 9, 2, "dark") fill(CX+3, CY-10, 9, 2, "dark") fill(CX-2, CY+2, 4, 3, "pink") fill(CX-4, CY+5, 8, 2, "dark") line(CX-13, CY, CX-22, CY-4, "dark") line(CX-13, CY+2, CX-22, CY, "dark") line(CX+13, CY, CX+22, CY-4, "dark") line(CX+13, CY+2, CX+22, CY, "dark") def annoyed_0(): """烦躁帧1:皱眉生气。""" cat_base() fill(CX-12, CY-3, 10, 5, "red") fill(CX+2, CY-3, 10, 5, "red") fill(CX-9, CY-2, 2, 2, "black") fill(CX+5, CY-2, 2, 2, "black") line(CX-13, CY-9, CX-4, CY-6, "dark") line(CX-13, CY-6, CX-4, CY-9, "dark") line(CX+4, CY-9, CX+13, CY-6, "dark") line(CX+4, CY-6, CX+13, CY-9, "dark") fill_circle(CX-15, CY+2, 3, "blush_r") fill_circle(CX+15, CY+2, 3, "blush_r") line(CX-6, CY+8, CX-2, CY+6, "dark") line(CX+6, CY+8, CX+2, CY+6, "dark") line(CX-R+4, CY-R, CX-R+10, CY-R-7, "dark") line(CX+R-4, CY-R, CX+R-10, CY-R-7, "dark") def annoyed_1(): """烦躁帧2:眯眼瞪视。""" cat_base() fill(CX-12, CY-3, 10, 2, "red") fill(CX+2, CY-3, 10, 2, "red") fill(CX-9, CY-3, 2, 2, "black") fill(CX+5, CY-3, 2, 2, "black") line(CX-13, CY-9, CX-4, CY-6, "dark") line(CX-13, CY-6, CX-4, CY-9, "dark") line(CX+4, CY-9, CX+13, CY-6, "dark") line(CX+4, CY-6, CX+13, CY-9, "dark") fill_circle(CX-15, CY+2, 3, "blush_r") fill_circle(CX+15, CY+2, 3, "blush_r") fill(CX-5, CY+6, 10, 2, "dark") line(CX-R+4, CY-R, CX-R+10, CY-R-7, "dark") line(CX+R-4, CY-R, CX+R-10, CY-R-7, "dark") def sleepy_0(): """困倦帧1:半闭眼打哈欠。""" cat_base() fill_circle(CX-9, CY-1, 5, "orange") fill_circle(CX+9, CY-1, 5, "orange") fill(CX-14, CY-1, 10, 5, "white") fill(CX+4, CY-1, 10, 5, "white") fill_circle(CX-9, CY, 1, "black") fill_circle(CX+9, CY, 1, "black") fill_circle(CX-15, CY+2, 3, "blush_y") fill_circle(CX+15, CY+2, 3, "blush_y") fill(CX-4, CY+5, 8, 6, "mouth") line(CX-4, CY+5, CX+4, CY+5, "dark") fill(CX-2, CY+7, 4, 3, "pink") line(CX-R+4, CY-R, CX-R-3, CY-R-7, "dark") line(CX+R-4, CY-R, CX+R+3, CY-R-7, "dark") def sleepy_1(): """困倦帧2:几乎闭眼 + ZZZ。""" cat_base() fill(CX-12, CY-2, 9, 2, "dark") fill(CX+3, CY-2, 9, 2, "dark") fill_circle(CX-15, CY+2, 3, "blush_y") fill_circle(CX+15, CY+2, 3, "blush_y") fill(CX-3, CY+5, 6, 3, "mouth") line(CX-3, CY+5, CX+3, CY+5, "dark") fill(CX-1, CY+6, 2, 2, "pink") line(CX-R+4, CY-R, CX-R-3, CY-R-7, "dark") line(CX+R-4, CY-R, CX+R+3, CY-R-7, "dark") # ZZZ 符号 for zx, zy, sz in [(16, -18, 3), (21, -23, 3), (26, -28, 3)]: circle(CX+zx, CY+zy, sz, "blue") # ── 导出 ──────────────────────────────────────────────────────────────────── FRAMES = [ ("idle", idle_0, idle_1), ("happy", happy_0, happy_1), ("focused", focused_0, focused_1), ("annoyed", annoyed_0, annoyed_1), ("sleepy", sleepy_0, sleepy_1), ] ROOT = "/Users/e2hang/hermes/code/ezvibe/assets/pet" os.makedirs(ROOT, exist_ok=True) for emotion, f0, f1 in FRAMES: out_dir = os.path.join(ROOT, emotion) os.makedirs(out_dir, exist_ok=True) for frame_idx, draw_fn in enumerate([f0, f1]): img = Image.new("RGBA", (W, H), (0, 0, 0, 0)) pixels = img.load() draw_fn() out_path = os.path.join(out_dir, f"pet_{emotion}_{frame_idx}.png") img.save(out_path) print(f" ✓ {out_path}") print(f"\n全部完成!10 张 PNG 已保存到 {ROOT}")