一、核心参数配置(常量定义)
python
# 画布尺寸(正方形,640x640,视觉更协调)
CANVAS_WIDTH = 640
CANVAS_HEIGHT = 640
# 画布中心坐标(自动计算,用于将爱心居中)
CANVAS_CENTER_X = CANVAS_WIDTH / 2 # 320
CANVAS_CENTER_Y = CANVAS_HEIGHT / 2 # 320
# 爱心缩放比例(控制爱心整体大小,值越大爱心越大)
IMAGE_ENLARGE = 11
# 爱心颜色(单一粉红色 #Fd798f,搭配黑色背景更突出)
HEART_COLOR = "#Fd798f"
这些参数是动画的基础配置,直接决定了爱心的大小、位置、颜色和画布尺寸。
二、核心数学函数(生成爱心形状与动态效果)
1. 爱心轮廓生成函数 heart_function(t)
python
def heart_function(t, shrink_ratio: float = IMAGE_ENLARGE):
# 笛卡尔心形曲线公式(核心:用数学计算生成爱心形状)
x = 16 * (sin(t) ** 3)
y = -(15 * cos(t) - 5 * cos(2 * t) - 2 * cos(3 * t) - cos(3*t)) # 微调后更圆润
# 缩放爱心(原始公式生成的爱心很小,需放大)
x *= shrink_ratio
y *= shrink_ratio
# 将爱心移动到画布中心(原始公式以原点为中心,需偏移到画布中间)
x += CANVAS_CENTER_X
y += CANVAS_CENTER_Y
return int(x), int(y)
- 原理 :通过参数
t
(范围 0~2π)计算爱心上的点坐标,t
遍历一周时,(x,y)
轨迹形成爱心形状。 - 微调细节 :Y 坐标公式中最后一项用
cos(3t)
替代了常见的cos(4t)
,让爱心轮廓更圆润。
2. 内部填充点生成函数 scatter_inside(x, y)
python
def scatter_inside(x, y, beta=0.15):
# 基于对数分布生成随机扩散比例(让点向中心聚集,避免均匀分布)
ratio_x = - beta * log(random.random())
ratio_y = - beta * log(random.random())
# 计算扩散距离(距离中心越远,扩散幅度越大)
dx = ratio_x * (x - CANVAS_CENTER_X)
dy = ratio_y * (y - CANVAS_CENTER_Y)
return x - dx, y - dy # 返回扩散后的点(向中心收缩)
- 作用:从爱心轮廓点出发,生成向内扩散的点,用于填充爱心内部,让爱心更饱满。
- 参数
beta
:控制扩散强度,值越大扩散范围越广(边缘点用beta=0.3
,中心填充点用beta=0.2
,实现 "边缘疏、中心密" 的效果)。
3. 外围光晕生成函数 shrink(x, y, ratio)
python
def shrink(x, y, ratio):
# 计算收缩力(距离中心越远,收缩力越强,让外围点形成环绕效果)
force = -1 / (((x - CANVAS_CENTER_X) **2 + (y - CANVAS_CENTER_Y)** 2) **0.6)
# 计算收缩距离(按比例调整,生成光晕)
dx = ratio * force * (x - CANVAS_CENTER_X)
dy = ratio * force * (y - CANVAS_CENTER_Y)
return x - dx, y - dy # 返回收缩后的点(形成外围光晕)
- 作用:生成爱心外围的 "光晕" 效果,让爱心边缘有渐变的扩散感。
4. 跳动周期控制函数 curve(p)
python
def curve(p):
# 正弦函数生成周期性值(控制心跳的"收缩-扩张"节奏)
return 2 * (2 * sin(4 * p)) / (2 * pi)
- 输出特性 :值随
p
周期性变化(范围约 -0.63~0.63),模拟心跳的节奏。
三、Heart
类(动画核心逻辑)
该类负责生成爱心的所有点集,并计算每帧的位置,实现 "跳动" 效果。
1. 初始化 __init__
python
def __init__(self, generate_frame=20):
self._points = set() # 爱心轮廓点
self._edge_diffusion_points = set() # 边缘扩散点
self._center_diffusion_points = set() # 中心填充点
self.all_points = {} # 存储每帧的所有点(key: 帧数,value: 点列表)
self.build(2000) # 生成基础点集
self.generate_frame = generate_frame # 动画周期(20帧)
for frame in range(generate_frame):
self.calc(frame) # 预计算每帧的点位置
- 核心:初始化三类点集,并提前计算动画周期内(20 帧)所有点的位置,保证动画流畅。
2. 生成基础点集 build
python
def build(self, number):
# 1. 生成2000个轮廓点(爱心的基本形状)
for _ in range(number):
t = random.uniform(0, 2 * pi) # 随机参数t
x, y = heart_function(t) # 计算轮廓点
self._points.add((x, y))
# 2. 生成边缘扩散点(每个轮廓点生成3个,共6000个,让边缘柔和)
for _x, _y in list(self._points):
for _ in range(3):
x, y = scatter_inside(_x, _y, 0.3) # 边缘扩散强度0.3
self._edge_diffusion_points.add((x, y))
# 3. 生成中心填充点(4000个,让爱心内部实心)
point_list = list(self._points)
for _ in range(4000):
x, y = random.choice(point_list) # 随机选轮廓点
x, y = scatter_inside(x, y, 0.2) # 中心扩散强度0.2(比边缘弱)
self._center_diffusion_points.add((x, y))
- 逻辑:通过 "轮廓点 + 边缘点 + 中心填充点" 的组合,让爱心从外到内都有足够的点,视觉上更饱满。
3. 计算每帧点位置 calc
(实现跳动的核心)
python
def calc(self, generate_frame):
# 1. 计算当前帧的跳动比例(基于曲线函数,控制缩放幅度)
ratio = 15 * curve(generate_frame / 15 * pi) # 放大15倍,增强跳动感
# 2. 计算外围光晕参数(随跳动变化,光晕时大时小)
halo_radius = int(4 + 6 * (1 + curve(generate_frame / 15 * pi))) # 光晕半径
halo_number = int(3000 + 4000 * abs(curve(generate_frame / 15 * pi) **2)) # 光晕点数量
all_points = [] # 存储当前帧的所有点
# 3. 生成外围光晕点
heart_halo_point = set() # 去重,避免重复点
for _ in range(halo_number):
t = random.uniform(0, 2 * pi)
x, y = heart_function(t, shrink_ratio=11.5) # 稍大的爱心轮廓
x, y = shrink(x, y, halo_radius) # 收缩成光晕
x += random.randint(-16, 16) # 随机偏移,让光晕更自然
y += random.randint(-16, 16)
size = random.choice((2, 2, 1)) # 光晕点大小
all_points.append((x, y, size))
# 4. 调整轮廓点位置(随跳动缩放)
for x, y in self._points:
x, y = self.calc_position(x, y, ratio) # 按比例调整位置
size = random.randint(1, 2) # 轮廓点大小
all_points.append((x, y, size))
# 5. 调整边缘扩散点位置
for x, y in self._edge_diffusion_points:
x, y = self.calc_position(x, y, ratio)
size = random.randint(1, 2)
all_points.append((x, y, size))
# 6. 调整中心填充点位置
for x, y in self._center_diffusion_points:
x, y = self.calc_position(x, y, ratio)
size = random.randint(1, 2)
all_points.append((x, y, size))
self.all_points[generate_frame] = all_points # 存储当前帧的点(注:此处可优化,移到循环外)
跳动原理 :通过 ratio
(随帧数周期性变化的缩放比例)调整所有点的位置,实现爱心 "放大 - 缩小" 的循环,模拟心跳。- 细节 **:光晕的大小和点数量也随跳动变化,增强视觉层次感。
4. 绘制当前帧 render
python
def render(self, render_canvas, render_frame):
# 遍历当前帧的所有点,用矩形绘制(效率高于圆形,视觉效果接近点)
for x, y, size in self.all_points[render_frame % self.generate_frame]:
render_canvas.create_rectangle(
x, y, x + size, y + size, # 矩形左上角和右下角坐标
width=0, # 无边框
fill=HEART_COLOR # 填充爱心颜色
)
- 作用:将当前帧的所有点绘制到画布上,形成爱心图案。
四、动画循环 draw
函数
python
def draw(main: Tk, render_canvas: Canvas, render_heart: Heart, render_frame=0):
render_canvas.delete('all') # 清空上一帧
render_heart.render(render_canvas, render_frame) # 绘制当前帧
# 160毫秒后绘制下一帧,形成动画(160ms/帧 → 约6帧/秒,节奏舒缓)
main.after(160, draw, main, render_canvas, render_heart, render_frame + 1)
- 核心:通过
after
方法循环调用自身,不断刷新画布,实现连续动画效果。
五、主程序入口
python
if __name__ == '__main__':
root = Tk() # 创建窗口
root.title("HYQ爱心代码") # 窗口标题(个性化定制)
canvas = Canvas(root, bg='black', height=CANVAS_HEIGHT, width=CANVAS_WIDTH) # 黑色画布
canvas.pack()
heart = Heart() # 初始化爱心
draw(root, canvas, heart) # 启动动画
root.mainloop() # 进入事件循环
- 功能:创建窗口、画布,初始化爱心对象并启动动画,最终显示跳动的爱心。
六、 总结
这段代码的核心逻辑是:
- 用笛卡尔心形曲线生成爱心的基础点集(轮廓 + 填充);
- 用正弦函数控制点的周期性缩放,实现 "心跳" 效果;
- 通过Tkinter 的画布循环刷新,形成连续动画。
代码特点:单一配色 + 黑色背景提升视觉质感,较慢的跳动节奏(160ms / 帧)更显温柔,适合作为个性化礼物(标题含 "HYQ",推测为定制昵称)。整体逻辑清晰,仅需优化一处冗余赋值(all_points
存储位置)即可更高效运行。
七、代码示例
python
import random
from math import sin,cos,pi,log
from tkinter import *
CANVAS_WIDTH = 640
CANVAS_HEIGHT = 640
CANVAS_CENTER_X = CANVAS_WIDTH / 2
CANVAS_CENTER_Y = CANVAS_HEIGHT / 2
IMAGE_ENLARGE = 11
HEART_COLOR = "#Fd798f"
def heart_function(t, shrink_ratio: float = IMAGE_ENLARGE):
x = 16 * (sin(t) ** 3)
y = -(15 * cos(t) - 5 * cos(2 * t) - 2 * cos(3 * t) - cos(3*t))
x *= shrink_ratio
y *= shrink_ratio
x += CANVAS_CENTER_X
y += CANVAS_CENTER_Y
return int(x), int(y)
def scatter_inside(x, y, beta=0.15):
ratio_x = - beta * log(random.random())
ratio_y = - beta * log(random.random())
dx = ratio_x * (x - CANVAS_CENTER_X)
dy = ratio_y * (y - CANVAS_CENTER_Y)
return x - dx, y - dy
def shrink(x, y, ratio):
force = -1 / (((x-CANVAS_CENTER_X) ** 2 +
(y - CANVAS_CENTER_Y) ** 2) ** 0.6)
dx = ratio * force * (x - CANVAS_CENTER_X)
dy = ratio * force * (y - CANVAS_CENTER_Y)
return x - dx, y - dy
def curve(p):
return 2 * (2 * sin(4 * p)) / (2 * pi)
class Heart:
def __init__(self, generate_frame=20):
self._points = set()
self._edge_diffusion_points = set()
self._center_diffusion_points = set()
self.all_points = {}
self.build(2000)
self.random_halo = 1000
self.generate_frame = generate_frame
for frame in range(generate_frame):
self.calc(frame)
def build(self, number):
for _ in range(number):
t = random.uniform(0, 2 * pi)
x, y = heart_function(t)
self._points.add((x, y))
for _x, _y in list(self._points):
for _ in range(3):
x, y = scatter_inside(_x, _y, 0.3)
self._edge_diffusion_points.add((x, y))
point_list = list(self._points)
for _ in range(4000):
x, y = random.choice(point_list)
x, y = scatter_inside(x, y, 0.2)
self._center_diffusion_points.add((x, y))
@staticmethod
def calc_position(x, y, ratio):
force = 1 / (((x - CANVAS_CENTER_X) ** 2 +
(y - CANVAS_CENTER_Y) ** 2) ** 0.520)
dx = ratio * force * (x - CANVAS_CENTER_X) + random.randint(-2, 2)
dy = ratio * force * (y - CANVAS_CENTER_Y) + random.randint(-2, 2)
return x - dx, y - dy
def calc(self,generate_frame):
ratio = 15 * curve(generate_frame / 15 * pi)
halo_radius = int(4 + 6 * (1 + curve(generate_frame / 15 * pi)))
halo_number = int(3000 + 4000 * abs(curve(generate_frame / 15 * pi) ** 2))
all_points = []
heart_halo_point = set()
for _ in range(halo_number):
t = random.uniform(0, 2 * pi)
x, y = heart_function(t, shrink_ratio=11.5)
x, y = shrink(x, y, halo_radius)
if (x, y) not in heart_halo_point:
heart_halo_point.add((x, y))
x += random.randint(-16, 16)
y += random.randint(-16, 16)
size = random.choice((2, 2, 1))
all_points.append((x, y, size))
for x, y in self._points:
x, y = self.calc_position(x, y, ratio)
size = random.randint(1, 2)
all_points.append((x, y, size))
for x, y in self._edge_diffusion_points:
x, y = self.calc_position(x, y, ratio)
size = random.randint(1, 2)
all_points.append((x, y, size))
# self.all_points[generate_frame] = all_points
for x, y in self._center_diffusion_points:
x, y = self.calc_position(x, y, ratio)
size = random.randint(1, 2)
all_points.append((x, y, size))
self.all_points[generate_frame] = all_points
def render(self, render_canvas, render_frame):
for x, y, size in self.all_points[render_frame % self.generate_frame]:
render_canvas.create_rectangle(x, y, x + size, y + size, width=0, fill=HEART_COLOR)
def draw(main: Tk, render_canvas: Canvas, render_heart: Heart, render_frame=0):
render_canvas.delete('all')
render_heart.render(render_canvas, render_frame)
main.after(160, draw, main, render_canvas, render_heart, render_frame + 1)
if __name__ == '__main__':
root = Tk()
root.title("HYQ爱心代码")
canvas = Canvas(root, bg='black', height=CANVAS_HEIGHT, width=CANVAS_WIDTH)
canvas.pack()
heart = Heart()
draw(root, canvas, heart)
root.mainloop()
八、效果图
