512 lines
19 KiB
Python
512 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
2333娘桌宠
|
||
基于 tkinter 的透明窗口桌面宠物,无需第三方依赖。
|
||
- 左键拖动移动
|
||
- 双击:开心跳跃 + "2333!" 气泡
|
||
- 右键:弹出菜单(说话 / 切换表情 / 置顶 / 退出)
|
||
- 自动呼吸、眨眼、偶尔随机吐槽
|
||
"""
|
||
|
||
import math
|
||
import random
|
||
import sys
|
||
import tkinter as tk
|
||
|
||
# ============== 颜色配置 ==============
|
||
COLOR_BG = '#FF00FF' # 透明色(纯品红,绘制中不使用)
|
||
COLOR_HAIR = '#00AEEC' # B站蓝
|
||
COLOR_HAIR_DARK = '#0090C8'
|
||
COLOR_HAIR_LIGHT = '#5FC8F2'
|
||
COLOR_SKIN = '#FFE6D5'
|
||
COLOR_SKIN_SHADOW = '#FAD0BC'
|
||
COLOR_BLUSH = '#FF9FB2'
|
||
COLOR_EYE = '#2EA9DF'
|
||
COLOR_EYE_DARK = '#1B7FB8'
|
||
COLOR_WHITE = '#FFFFFF'
|
||
COLOR_DRESS = '#F2FAFF'
|
||
COLOR_DRESS_BLUE = '#00AEEC'
|
||
COLOR_DRESS_DARK = '#0090C8'
|
||
COLOR_MOUTH = '#C8485A'
|
||
COLOR_MOUTH_INNER = '#FF8A9A'
|
||
COLOR_TONGUE = '#FFB6C1'
|
||
COLOR_ACCENT = '#FB7299' # B站粉
|
||
COLOR_DARK = '#3A3A3A'
|
||
COLOR_SCREEN = '#1E2233' # 额头成像仪屏幕
|
||
|
||
# ============== 画布尺寸 ==============
|
||
WIDTH = 240
|
||
HEIGHT = 310
|
||
CX = WIDTH // 2
|
||
FRAME_MS = 33 # ~30fps
|
||
|
||
# ============== 随机吐槽语 ==============
|
||
IDLE_MESSAGES = [
|
||
'2333~', '哈哈哈~', '干杯!(´・ω・`)', '哔哩哔哩☆', '今天也要元气满满哦!',
|
||
'戳我干嘛 (`ε´)', '最喜欢姐姐了~', '需要充电...zzZ', '2233娘最强!',
|
||
'233333333', '再看就把你吃掉!', '帮我插个插座?', '咕~ 咕~',
|
||
'今天视频审核完了吗?', '点击这里投喂电池 →',
|
||
]
|
||
|
||
|
||
class DesktopPet:
|
||
"""2333娘桌宠主类"""
|
||
|
||
def __init__(self, blocking=True, on_exit=None):
|
||
"""blocking=True 时调用 mainloop 阻塞;False 时由外部驱动 root.update()。
|
||
on_exit: 退出时的回调(用于通知调用者恢复)。"""
|
||
self._blocking = blocking
|
||
self._on_exit = on_exit
|
||
self._exited = False
|
||
self.root = tk.Tk()
|
||
self.root.title('2333娘')
|
||
# 初始位置:屏幕右下角
|
||
sx = self.root.winfo_screenwidth() - WIDTH - 30
|
||
sy = self.root.winfo_screenheight() - HEIGHT - 80
|
||
self.root.geometry(f'{WIDTH}x{HEIGHT}+{sx}+{sy}')
|
||
self.root.overrideredirect(True)
|
||
self.root.attributes('-topmost', True)
|
||
self.root.config(bg=COLOR_BG)
|
||
try:
|
||
self.root.attributes('-transparentcolor', COLOR_BG)
|
||
except tk.TclError:
|
||
pass
|
||
|
||
self.canvas = tk.Canvas(
|
||
self.root, width=WIDTH, height=HEIGHT,
|
||
bg=COLOR_BG, highlightthickness=0, bd=0
|
||
)
|
||
self.canvas.pack()
|
||
|
||
# ----- 动画状态 -----
|
||
self.t = 0.0
|
||
self.dt = FRAME_MS / 1000.0
|
||
self.bob_y = 0.0
|
||
self.jump_y = 0.0
|
||
self.jump_vel = 0.0
|
||
|
||
# 眨眼
|
||
self.blink_phase = 0.0 # 0=睁眼;>0=正在眨眼
|
||
self.next_blink = random.uniform(2.0, 5.0)
|
||
self.eye_open = 1.0
|
||
|
||
# 表情与说话
|
||
self.expression = 'laugh' # laugh / happy / shy / wink
|
||
self.speech_text = ''
|
||
self.speech_timer = 0.0
|
||
self.idle_countdown = random.uniform(6.0, 12.0)
|
||
|
||
# 拖动
|
||
self._drag_dx = 0
|
||
self._drag_dy = 0
|
||
self._dragging = False
|
||
|
||
# 互动粒子(爱心/星星)
|
||
self.particles = [] # list of dict
|
||
|
||
self._build_menu()
|
||
self._bind_events()
|
||
self._schedule_idle_speech()
|
||
self._tick()
|
||
if self._blocking:
|
||
self.root.mainloop()
|
||
|
||
# ================= 菜单 =================
|
||
def _build_menu(self):
|
||
self.menu = tk.Menu(self.root, tearoff=0)
|
||
self.menu.add_command(label='说句话 (2333~)', command=self._say_random)
|
||
self.menu.add_separator()
|
||
self.menu.add_command(label='表情:大笑', command=lambda: self._set_expr('laugh'))
|
||
self.menu.add_command(label='表情:开心', command=lambda: self._set_expr('happy'))
|
||
self.menu.add_command(label='表情:害羞', command=lambda: self._set_expr('shy'))
|
||
self.menu.add_command(label='表情:眨眼', command=lambda: self._set_expr('wink'))
|
||
self.menu.add_separator()
|
||
self.menu.add_command(label='跳一下!', command=self._jump)
|
||
self.menu.add_command(label='置顶 / 取消置顶', command=self._toggle_topmost)
|
||
self.menu.add_separator()
|
||
self.menu.add_command(label='Live2D 模式 ✨', command=self._switch_to_live2d)
|
||
self.menu.add_separator()
|
||
self.menu.add_command(label='退出 (。•́︿•̀。)', command=self._quit)
|
||
|
||
# ================= 事件绑定 =================
|
||
def _bind_events(self):
|
||
self.canvas.bind('<ButtonPress-1>', self._on_press)
|
||
self.canvas.bind('<B1-Motion>', self._on_drag)
|
||
self.canvas.bind('<ButtonRelease-1>', self._on_release)
|
||
self.canvas.bind('<ButtonPress-3>', self._on_right)
|
||
self.canvas.bind('<Double-Button-1>', self._on_double)
|
||
self.root.bind('<Escape>', lambda e: self._quit())
|
||
|
||
# ----- 鼠标交互 -----
|
||
def _on_press(self, e):
|
||
self._dragging = True
|
||
self._drag_dx = e.x_root - self.root.winfo_x()
|
||
self._drag_dy = e.y_root - self.root.winfo_y()
|
||
|
||
def _on_drag(self, e):
|
||
if self._dragging:
|
||
x = e.x_root - self._drag_dx
|
||
y = e.y_root - self._drag_dy
|
||
self.root.geometry(f'+{x}+{y}')
|
||
|
||
def _on_release(self, e):
|
||
self._dragging = False
|
||
|
||
def _on_right(self, e):
|
||
try:
|
||
self.menu.tk_popup(e.x_root, e.y_root)
|
||
finally:
|
||
self.menu.grab_release()
|
||
|
||
def _on_double(self, e):
|
||
self._jump()
|
||
self._say('2333!')
|
||
|
||
# ----- 菜单动作 -----
|
||
def _set_expr(self, expr):
|
||
self.expression = expr
|
||
self._say({'laugh': '2333~', 'happy': '嘿嘿~', 'shy': '呜...>//<', 'wink': '★~'}[expr])
|
||
|
||
def _say(self, text):
|
||
self.speech_text = text
|
||
self.speech_timer = 2.4
|
||
|
||
def _say_random(self):
|
||
self._say(random.choice(IDLE_MESSAGES))
|
||
|
||
def _jump(self):
|
||
if self.jump_y == 0 and self.jump_vel == 0:
|
||
self.jump_vel = -13.0
|
||
|
||
def _toggle_topmost(self):
|
||
self.root.attributes('-topmost', not self.root.attributes('-topmost'))
|
||
|
||
def _switch_to_live2d(self):
|
||
"""切换回 Live2D 模式(非阻塞模式下通知调用者恢复)"""
|
||
self._do_exit()
|
||
|
||
def _do_exit(self):
|
||
"""统一退出处理"""
|
||
if self._exited:
|
||
return
|
||
self._exited = True
|
||
try:
|
||
self.root.destroy()
|
||
except Exception:
|
||
pass
|
||
if self._on_exit:
|
||
self._on_exit()
|
||
elif self._blocking:
|
||
sys.exit(0)
|
||
|
||
def _quit(self):
|
||
self._do_exit()
|
||
|
||
def _schedule_idle_speech(self):
|
||
# 用随机定时器在 _tick 中处理,这里不需要额外线程
|
||
pass
|
||
|
||
# ================= 主循环 =================
|
||
def _tick(self):
|
||
self.t += self.dt
|
||
|
||
# 呼吸浮动
|
||
self.bob_y = math.sin(self.t * 2.2) * 3.0
|
||
|
||
# 跳跃物理
|
||
if self.jump_y < 0 or self.jump_vel < 0:
|
||
self.jump_vel += 1.1 # 重力
|
||
self.jump_y += self.jump_vel
|
||
if self.jump_y >= 0:
|
||
self.jump_y = 0.0
|
||
self.jump_vel = 0.0
|
||
|
||
# 眨眼计时
|
||
if self.blink_phase == 0.0:
|
||
self.next_blink -= self.dt
|
||
if self.next_blink <= 0:
|
||
self.blink_phase = 0.0001
|
||
else:
|
||
self.blink_phase += self.dt
|
||
# 0~0.05 闭眼,0.05~0.13 闭着,0.13~0.20 睁眼
|
||
p = self.blink_phase
|
||
if p < 0.05:
|
||
self.eye_open = 1.0 - (p / 0.05)
|
||
elif p < 0.13:
|
||
self.eye_open = 0.0
|
||
elif p < 0.20:
|
||
self.eye_open = (p - 0.13) / 0.07
|
||
else:
|
||
self.eye_open = 1.0
|
||
self.blink_phase = 0.0
|
||
self.next_blink = random.uniform(2.5, 6.5)
|
||
self.eye_open = max(0.0, min(1.0, self.eye_open))
|
||
|
||
# wink 表情强制一只眼闭着
|
||
if self.expression == 'wink':
|
||
self._wink_left_open = True
|
||
|
||
# 说话气泡计时
|
||
if self.speech_timer > 0:
|
||
self.speech_timer -= self.dt
|
||
if self.speech_timer <= 0:
|
||
self.speech_text = ''
|
||
|
||
# 随机吐槽
|
||
self.idle_countdown -= self.dt
|
||
if self.idle_countdown <= 0:
|
||
self.idle_countdown = random.uniform(10.0, 20.0)
|
||
if not self.speech_text and random.random() < 0.7:
|
||
self._say(random.choice(IDLE_MESSAGES))
|
||
|
||
# 粒子更新
|
||
self._update_particles()
|
||
|
||
self._draw()
|
||
self.root.after(FRAME_MS, self._tick)
|
||
|
||
# ================= 绘制 =================
|
||
def _draw(self):
|
||
c = self.canvas
|
||
c.delete('all')
|
||
oy = self.bob_y + self.jump_y
|
||
|
||
self._draw_hair_back(oy)
|
||
self._draw_body(oy)
|
||
self._draw_face(oy)
|
||
self._draw_forehead_device(oy)
|
||
self._draw_blush(oy)
|
||
self._draw_eyes(oy)
|
||
self._draw_mouth(oy)
|
||
self._draw_hair_front(oy)
|
||
self._draw_ahoge(oy)
|
||
self._draw_particles()
|
||
self._draw_speech(oy)
|
||
|
||
# ----- 后侧头发 -----
|
||
def _draw_hair_back(self, oy):
|
||
c = self.canvas
|
||
# 大块蓝色后发
|
||
c.create_oval(CX - 96, 36 + oy, CX + 96, 196 + oy, fill=COLOR_HAIR, outline=COLOR_HAIR_DARK, width=2)
|
||
# 两侧散发
|
||
c.create_oval(CX - 100, 120 + oy, CX - 60, 230 + oy, fill=COLOR_HAIR, outline=COLOR_HAIR_DARK, width=2)
|
||
c.create_oval(CX + 60, 120 + oy, CX + 100, 230 + oy, fill=COLOR_HAIR, outline=COLOR_HAIR_DARK, width=2)
|
||
|
||
# ----- 脸 -----
|
||
def _draw_face(self, oy):
|
||
c = self.canvas
|
||
c.create_oval(CX - 80, 44 + oy, CX + 80, 196 + oy, fill=COLOR_SKIN, outline=COLOR_SKIN_SHADOW, width=2)
|
||
|
||
# ----- 额头成像仪(33娘特征)-----
|
||
def _draw_forehead_device(self, oy):
|
||
c = self.canvas
|
||
cx, cy = CX, 80 + oy
|
||
# 外圈
|
||
c.create_oval(cx - 16, cy - 16, cx + 16, cy + 16, fill=COLOR_DRESS_BLUE, outline=COLOR_DRESS_DARK, width=2)
|
||
# 屏幕
|
||
c.create_oval(cx - 11, cy - 11, cx + 11, cy + 11, fill=COLOR_SCREEN, outline='')
|
||
# 屏幕上的小光点(呼吸)
|
||
glow = 0.5 + 0.5 * math.sin(self.t * 3)
|
||
col = self._mix(COLOR_HAIR_LIGHT, COLOR_WHITE, glow)
|
||
c.create_oval(cx - 4, cy - 4, cx + 4, cy + 4, fill=col, outline='')
|
||
|
||
# ----- 腮红 -----
|
||
def _draw_blush(self, oy):
|
||
c = self.canvas
|
||
alpha = 1.0
|
||
if self.expression == 'shy':
|
||
alpha = 1.4
|
||
col = self._mix(COLOR_SKIN, COLOR_BLUSH, min(1.0, 0.6 * alpha))
|
||
c.create_oval(CX - 58, 138 + oy, CX - 28, 156 + oy, fill=col, outline='')
|
||
c.create_oval(CX + 28, 138 + oy, CX + 58, 156 + oy, fill=col, outline='')
|
||
|
||
# ----- 眼睛 -----
|
||
def _draw_eyes(self, oy):
|
||
c = self.canvas
|
||
lex, rex = CX - 34, CX + 34
|
||
ey = 120 + oy
|
||
open_f = self.eye_open
|
||
|
||
wink = (self.expression == 'wink')
|
||
left_open = open_f
|
||
right_open = 0.0 if wink else open_f
|
||
|
||
for (ex, op) in ((lex, left_open), (rex, right_open)):
|
||
if op > 0.15:
|
||
self._draw_open_eye(ex, ey, op)
|
||
else:
|
||
# 闭眼弧线
|
||
c.create_arc(ex - 14, ey - 6, ex + 14, ey + 8, start=20, extent=140,
|
||
style='arc', outline=COLOR_DARK, width=2.5)
|
||
|
||
def _draw_open_eye(self, ex, ey, op):
|
||
c = self.canvas
|
||
# 眼白
|
||
ry = 22 * op
|
||
c.create_oval(ex - 16, ey - ry, ex + 16, ey + ry, fill=COLOR_WHITE, outline=COLOR_DARK, width=1.5)
|
||
# 虹膜
|
||
ir = 12
|
||
c.create_oval(ex - ir, ey - ir, ex + ir, ey + ir, fill=COLOR_EYE, outline='')
|
||
# 瞳孔
|
||
c.create_oval(ex - 6, ey - 6, ex + 6, ey + 6, fill=COLOR_DARK, outline='')
|
||
# 高光
|
||
c.create_oval(ex - 9, ey - 9, ex - 3, ey - 3, fill=COLOR_WHITE, outline='')
|
||
c.create_oval(ex + 3, ey + 2, ex + 7, ey + 6, fill=COLOR_WHITE, outline='')
|
||
|
||
# ----- 嘴 -----
|
||
def _draw_mouth(self, oy):
|
||
c = self.canvas
|
||
mx, my = CX, 158 + oy
|
||
expr = self.expression
|
||
if expr == 'laugh':
|
||
# 大笑:张开嘴
|
||
w = 20 + 3 * math.sin(self.t * 8)
|
||
c.create_oval(mx - w, my - 6, mx + w, my + 14, fill=COLOR_MOUTH_INNER, outline=COLOR_MOUTH, width=2)
|
||
# 舌头
|
||
c.create_oval(mx - 10, my + 2, mx + 10, my + 14, fill=COLOR_TONGUE, outline='')
|
||
# 上唇线
|
||
c.create_arc(mx - w, my - 12, mx + w, my + 6, start=200, extent=140, style='arc', outline=COLOR_MOUTH, width=2)
|
||
elif expr == 'happy':
|
||
# 微笑弧
|
||
c.create_arc(mx - 14, my - 8, mx + 14, my + 10, start=20, extent=140, style='arc', outline=COLOR_MOUTH, width=2.5)
|
||
elif expr == 'shy':
|
||
# 小抿嘴
|
||
c.create_line(mx - 8, my, mx + 8, my, fill=COLOR_MOUTH, width=2.5, smooth=True)
|
||
c.create_arc(mx - 6, my - 4, mx + 6, my + 4, start=200, extent=140, style='arc', outline=COLOR_MOUTH, width=2)
|
||
else: # wink
|
||
c.create_arc(mx - 12, my - 6, mx + 12, my + 12, start=20, extent=140, style='arc', outline=COLOR_MOUTH, width=2.5)
|
||
|
||
# ----- 前侧头发(刘海)-----
|
||
def _draw_hair_front(self, oy):
|
||
c = self.canvas
|
||
# 头顶发盖
|
||
c.create_oval(CX - 86, 30 + oy, CX + 86, 120 + oy, fill=COLOR_HAIR, outline=COLOR_HAIR_DARK, width=2)
|
||
# 刘海锯齿
|
||
bang_pts = []
|
||
tips = [
|
||
(CX - 80, 96), (CX - 64, 70), (CX - 48, 96),
|
||
(CX - 30, 74), (CX - 12, 98), (CX + 6, 76),
|
||
(CX + 24, 96), (CX + 44, 72), (CX + 62, 96),
|
||
(CX + 80, 78),
|
||
]
|
||
top_y = 44 + oy
|
||
# 沿头顶弧线生成上沿
|
||
for i, (tx, ty) in enumerate(tips):
|
||
bang_pts.append((tx, ty + oy))
|
||
# 用多边形画几缕刘海
|
||
for i in range(0, len(tips) - 1, 2):
|
||
x1, y1 = tips[i]
|
||
x2, y2 = tips[i + 1]
|
||
xm, ym = (x1 + x2) / 2, max(y1, y2) + 18
|
||
c.create_polygon(x1, top_y, x1, y1 + oy, xm, ym + oy, x2, y2 + oy, x2, top_y,
|
||
fill=COLOR_HAIR, outline=COLOR_HAIR_DARK, width=1.5)
|
||
# 两侧鬓发
|
||
c.create_polygon(CX - 86, 60 + oy, CX - 92, 170 + oy, CX - 64, 150 + oy, CX - 60, 70 + oy,
|
||
fill=COLOR_HAIR, outline=COLOR_HAIR_DARK, width=1.5)
|
||
c.create_polygon(CX + 86, 60 + oy, CX + 92, 170 + oy, CX + 64, 150 + oy, CX + 60, 70 + oy,
|
||
fill=COLOR_HAIR, outline=COLOR_HAIR_DARK, width=1.5)
|
||
|
||
# ----- 呆毛 -----
|
||
def _draw_ahoge(self, oy):
|
||
c = self.canvas
|
||
sway = math.sin(self.t * 3) * 4
|
||
# 一根弯曲呆毛
|
||
c.create_line(CX, 30 + oy, CX - 6 + sway, 6 + oy, CX + 6 + sway, 2 + oy,
|
||
fill=COLOR_HAIR_DARK, width=4, smooth=True, capstyle='round')
|
||
|
||
# ----- 身体 / 裙子 -----
|
||
def _draw_body(self, oy):
|
||
c = self.canvas
|
||
# 脖子
|
||
c.create_rectangle(CX - 14, 186 + oy, CX + 14, 210 + oy, fill=COLOR_SKIN, outline=COLOR_SKIN_SHADOW, width=1.5)
|
||
# 裙子上半(白色)
|
||
c.create_polygon(CX - 44, 210 + oy, CX + 44, 210 + oy, CX + 54, 270 + oy, CX - 54, 270 + oy,
|
||
fill=COLOR_DRESS, outline=COLOR_DRESS_DARK, width=2)
|
||
# 蓝色领结/缎带
|
||
c.create_polygon(CX - 24, 206 + oy, CX + 24, 206 + oy, CX + 18, 224 + oy, CX - 18, 224 + oy,
|
||
fill=COLOR_DRESS_BLUE, outline=COLOR_DRESS_DARK, width=1.5)
|
||
c.create_oval(CX - 6, 208 + oy, CX + 6, 220 + oy, fill=COLOR_ACCENT, outline='')
|
||
# 裙摆蓝色边
|
||
c.create_polygon(CX - 54, 262 + oy, CX + 54, 262 + oy, CX + 58, 276 + oy, CX - 58, 276 + oy,
|
||
fill=COLOR_DRESS_BLUE, outline=COLOR_DRESS_DARK, width=1.5)
|
||
# 手臂
|
||
c.create_oval(CX - 60, 214 + oy, CX - 40, 250 + oy, fill=COLOR_DRESS, outline=COLOR_DRESS_DARK, width=1.5)
|
||
c.create_oval(CX + 40, 214 + oy, CX + 60, 250 + oy, fill=COLOR_DRESS, outline=COLOR_DRESS_DARK, width=1.5)
|
||
# 手
|
||
c.create_oval(CX - 62, 244 + oy, CX - 44, 262 + oy, fill=COLOR_SKIN, outline=COLOR_SKIN_SHADOW, width=1.5)
|
||
c.create_oval(CX + 44, 244 + oy, CX + 62, 262 + oy, fill=COLOR_SKIN, outline=COLOR_SKIN_SHADOW, width=1.5)
|
||
|
||
# ----- 粒子(爱心)-----
|
||
def _spawn_hearts(self, n=6):
|
||
for _ in range(n):
|
||
self.particles.append({
|
||
'x': CX + random.uniform(-30, 30),
|
||
'y': 110 + random.uniform(-10, 10),
|
||
'vx': random.uniform(-1.2, 1.2),
|
||
'vy': random.uniform(-2.5, -1.0),
|
||
'life': 1.0,
|
||
'size': random.uniform(8, 14),
|
||
})
|
||
|
||
def _update_particles(self):
|
||
for p in self.particles:
|
||
p['x'] += p['vx']
|
||
p['y'] += p['vy']
|
||
p['vy'] += 0.04
|
||
p['life'] -= self.dt * 0.8
|
||
self.particles = [p for p in self.particles if p['life'] > 0]
|
||
|
||
def _draw_particles(self):
|
||
c = self.canvas
|
||
for p in self.particles:
|
||
s = p['size'] * max(0.2, p['life'])
|
||
x, y = p['x'], p['y']
|
||
c.create_text(x, y, text='♥', font=('Segoe UI Symbol', int(s)), fill=COLOR_ACCENT)
|
||
|
||
# ----- 说话气泡 -----
|
||
def _draw_speech(self, oy):
|
||
if not self.speech_text:
|
||
return
|
||
c = self.canvas
|
||
text = self.speech_text
|
||
font = ('微软雅黑', 12, 'bold')
|
||
# 估算宽度
|
||
w = max(70, len(text) * 14 + 26)
|
||
h = 36
|
||
x = CX
|
||
y = 6 + oy
|
||
# 气泡圆角矩形
|
||
pad = 8
|
||
c.create_rectangle(x - w / 2, y, x + w / 2, y + h, fill=COLOR_WHITE, outline=COLOR_ACCENT, width=2)
|
||
# 尾巴
|
||
c.create_polygon(x - 8, y + h - 1, x + 8, y + h - 1, x, y + h + 12,
|
||
fill=COLOR_WHITE, outline=COLOR_ACCENT, width=2)
|
||
# 遮掉尾巴上沿的边线
|
||
c.create_line(x - 7, y + h, x + 7, y + h, fill=COLOR_WHITE, width=3)
|
||
c.create_text(x, y + h / 2 + 1, text=text, fill=COLOR_ACCENT, font=font)
|
||
|
||
# ================= 工具 =================
|
||
@staticmethod
|
||
def _mix(c1, c2, t):
|
||
"""简单颜色线性混合,t in [0,1]"""
|
||
t = max(0.0, min(1.0, t))
|
||
|
||
def parse(h):
|
||
h = h.lstrip('#')
|
||
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
||
|
||
r1, g1, b1 = parse(c1)
|
||
r2, g2, b2 = parse(c2)
|
||
r = int(r1 + (r2 - r1) * t)
|
||
g = int(g1 + (g2 - g1) * t)
|
||
b = int(b1 + (b2 - b1) * t)
|
||
return f'#{r:02x}{g:02x}{b:02x}'
|
||
|
||
def _on_double_click_heart(self):
|
||
"""双击时额外撒爱心(供扩展)"""
|
||
self._spawn_hearts(8)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
DesktopPet()
|