Python爱心粒子代码解析:粒子系统与贝塞尔曲线实战

发布时间:2026/10/28 14:05:47
Python爱心粒子代码解析:粒子系统与贝塞尔曲线实战 简介这是一份面向Python初学者与图形编程爱好者的爱心粒子效果实现文档围绕粒子系统、三次贝塞尔曲线绘制与动画渲染展开帮助读者理解如何用代码生成动态爱心图形。压缩包内共1个docx文件约39KB以图文与代码示例形式组织内容便于对照阅读与本地复现。文档系统梳理了粒子类的属性设计、爱心轮廓的曲线构造、动画循环与透明度衰减等关键环节并给出基于pygame库的完整示例代码涵盖HeartParticle类与bezier_curve函数的具体写法读者可据此理解粒子运动规律与曲线渲染思路并在此基础上调整参数实现起伏、扩散等变化效果。目前已有7710人学习适合希望入门粒子动画、积累图形绘制经验的学习者参考。1. 一份能直接跑的 Python 爱心粒子代码到底值不值得下拿到这份python爱心粒子代码压缩包的时候我第一反应是把它当玩具——毕竟网上“Python 画爱心”的代码一抓一大把多数是几行 turtle 画个静态轮廓跑完就删。但这份不一样它用 pygame 搭了一个粒子系统粒子从爱心轮廓附近随机喷出、带速度与加速度、有生命周期衰减配合三次贝塞尔曲线描边跑起来是动态的。对想练手粒子系统、想给课程设计加个视觉彩蛋、或者单纯想搞明白“爱心曲线怎么用数学描述”的人来说这份代码有真实的可读价值。它不解决业务问题但它把“粒子类 贝塞尔采样 主循环渲染”这条最小闭环讲全了适合 Python 3.x 环境、装得上 pygame 的机器新手能跑通熟手能改参数。2. 环境与依赖pygame 装不上时先看这几处2.1 运行前的最小环境清单这份代码只依赖标准库random、math和第三方库pygame没有 numpy、没有 OpenGL门槛很低。但“低门槛”不等于“零翻车”我见过太多人卡在第一步。先把环境对齐项目要求说明Python3.0 以上建议 3.83.11代码用了 f-string 之外的旧式写法3.12 也能跑pygame2.x 任意小版本1.x 的draw.circle对 alpha 支持不稳别用显示环境有桌面窗口纯命令行服务器跑不起来会报 display 错误分辨率800×600 起步代码里set_mode((800, 600))写死改窗口要同步改坐标安装就一条命令但国内网络下经常卡在下载 wheel 上# 先确认 Python 版本低于 3.8 建议升级 python --version # 安装 pygame指定国内镜像源加速 pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple # 验证安装是否成功能打印版本号即通过 python -c import pygame; print(pygame.version.ver)逻辑说明pip install后面跟镜像源参数是为了绕开默认源偶发的超时最后一行用-c直接执行导入语句比进交互式环境快能立刻暴露“装了但导入失败”的问题。参数上如果你用的是 conda 环境把pip换成conda install -c conda-forge pygame更稳避免和系统 Python 混装。2.2 把代码落成文件并跑起来新建一个heart_particles.py把正文里的完整代码粘进去。注意原文代码里append_particles用到了math.sin、math.cos但顶部只import pygame, random这是原代码的一个真实缺口直接跑会报NameError: name math is not defined。补一行导入即可import pygame import random import math # 原代码遗漏append_particles 里用到 sin/cos # 初始化放在最前面避免后续 font.init 重复调用 pygame.init() screen pygame.display.set_mode((800, 600)) pygame.display.set_caption(Python Love) clock pygame.time.Clock()逻辑说明pygame.init()会连带初始化字体、显示等子系统原文单独调pygame.font.init()是冗余的但不影响运行。set_mode返回的 surface 就是主画布后面所有draw都往它上面画。参数上窗口尺寸 800×600 和后面draw_heart(400, 325, 3)的圆心坐标是绑定的——你把窗口改成 1000×800爱心就会偏到左上角必须同步改圆心。跑起来后你会看到一个白色背景上红色粒子从爱心区域向外飘散并逐渐消失同时白色贝塞尔曲线勾出爱心轮廓。如果窗口一闪就退检查主循环里for event in pygame.event.get()是否被正确保留——有人为了“精简”把事件循环删了窗口就失去响应。3. 粒子类与贝塞尔曲线这份代码的两个技术核心3.1 HeartParticle 类速度、加速度、生命周期的三件套粒子系统的骨架就在HeartParticle这个类里。它把每个粒子当成一个独立对象持有位置pos、速度vel、加速度acc、剩余寿命lifespan四个状态move()每帧更新状态draw()根据寿命算透明度并画圆。这是最朴素的欧拉积分没有重力场、没有碰撞但足够撑起“飘散”的观感。class HeartParticle(): def __init__(self, pos): self.pos [pos[0], pos[1]] # 初始速度-3.5 ~ 3.5 像素/帧除以 10 是为了让数值落在合理区间 self.vel [float(random.randint(-35, 35)) / 10, float(random.randint(-35, 35)) / 10] # 加速度-0.05 ~ 0.05量级很小作用是让轨迹轻微弯曲 self.acc [float(random.randint(-10, 10)) / 200, float(random.randint(-10, 10)) / 200] # 寿命 10~50 帧决定粒子存活时长和淡出速度 self.lifespan random.randint(10, 50) def move(self): self.vel[0] self.acc[0] # 速度累加加速度 self.vel[1] self.acc[1] self.pos[0] self.vel[0] # 位置累加速度 self.pos[1] self.vel[1] self.lifespan - 1 # 每帧扣寿命 def draw(self, window): if not self.lifespan: return False # 寿命耗尽通知外部移除 alpha int(float(self.lifespan) / 50 * 255) # 寿命映射到透明度 pygame.draw.circle(window, (255, 0, 0, alpha), [int(self.pos[0]), int(self.pos[1])], 6) return True逻辑说明move()里先更新速度再更新位置这是显式欧拉法的标准顺序顺序反了轨迹会略有偏差但不致命。draw()返回布尔值是个巧妙设计——外部用列表推导[p for p in particles if p.draw(screen)]一次性完成“绘制 剔除死亡粒子”省掉单独的清理循环。参数上lifespan上限 50 和alpha计算里的除数 50 必须一致你把寿命改成 100 却忘了改除数粒子就会在寿命过半时透明度就归零看起来“提前消失”。半径 6 是硬编码想要更细腻的粒子把它调到 23同时把append_particles里的生成数量从 15 提到 40 左右否则画面会变稀疏。3.2 贝塞尔曲线采样爱心轮廓是怎么算出来的爱心不是贴图是用六组控制点分别采样三次贝塞尔曲线再逐点连线画出来的。bezier_curve是个递归实现每次把相邻控制点按t做线性插值点集缩一个直到只剩一个点那就是曲线在t处的坐标。draw_heart里对每组控制点跑 100 次采样t从 0 到 1把相邻采样点用draw.line连起来形成平滑轮廓。def bezier_curve(points, time): if len(points) 1: return points # 递归出口只剩一个点 new_points [] for i in range(len(points) - 1): # 相邻两点线性插值time 越接近 1 越靠近后一个点 x (1 - time) * points[i][0] time * points[i 1][0] y (1 - time) * points[i][1] time * points[i 1][1] new_points.append([x, y]) return bezier_curve(new_points, time) # 递归降阶 def draw_heart(cx, cy, size): x cx - 32 * size y cy - 24 * size # 六组控制点坐标是归一化后的相对值乘以 size 缩放 points1 [(0.5, 0.2), (0.43, -0.05), (0, -0.05)] points2 [(0, -0.05), (-0.43, -0.05), (-0.5, 0.2)] # ... 其余四组省略结构相同 for i in range(100): t float(i) / 100 p1 bezier_curve(points1, t)[0] # 把归一化坐标映射回屏幕坐标x 64*size*p[0] pygame.draw.line(window, (255, 255, 255), [x 64 * size * p1[0], y 48 * size * p1[1]], [...], 3)逻辑说明控制点坐标都在 -0.50.5 之间是“单位爱心”的归一化描述乘以64*size和48*size才落到像素空间。size3时爱心宽度约 192 像素在 800 宽窗口里居中偏小想要更大就调size但注意draw_heart末尾还有一句pygame.draw.circle(window, (255,0,0), [cx, cy], int(50*size), 1)那是画一个红色细圆环size调大后圆环会盖住爱心建议直接删掉或把半径系数改小。参数上采样数 100 是精度和性能的折中降到 30 轮廓会有明显折角升到 300 肉眼几乎无差别但每帧多画 1200 条线低配机器会掉帧。4. 主循环与粒子生成让画面动起来的调度逻辑4.1 主循环的四步节奏主循环是整份代码的“发动机”顺序是处理事件 → 更新并绘制粒子 → 补充新粒子 → 清屏重绘爱心 → 刷新显示 → 锁帧。这个顺序里有两个容易踩的点清屏必须在画爱心之前否则爱心被上一帧的粒子残影盖住clock.tick(40)决定帧率40 帧下粒子运动速度是“每帧 3.5 像素”换算成每秒约 140 像素观感偏快想要更飘逸就降到 30。done False particles [] while not done: for event in pygame.event.get(): if event.type pygame.QUIT: done True # 第一步更新所有粒子位置并绘制同时剔除死亡粒子 particles draw_particles(screen, particles) # 第二步每帧补充 15 个新粒子围绕爱心中心生成 append_particles(particles, [400, 300], 1) # 第三步清屏白色再画爱心轮廓 screen.fill((255, 255, 255)) draw_heart(400, 325, 3) # 第四步刷新显示并锁帧 pygame.display.flip() clock.tick(40)逻辑说明draw_particles内部对每个粒子先move()再draw()返回存活列表这一步同时完成“运动 渲染 回收”。append_particles每帧固定生成 15 个配合平均 30 帧寿命稳态下屏幕上约 450 个粒子这个量级 pygame 完全扛得住。参数上clock.tick(40)的 40 是帧率上限不是固定步长——机器慢的时候实际帧率会低于 40粒子运动就会变慢这是 pygame 的固有特性想要帧率无关的运动得引入delta time但这份代码没做属于可接受的简化。4.2 append_particles 的生成策略与一个隐藏 bug粒子生成用的是正态分布半径加均匀分布角度random.normalvariate(0, 10)让粒子集中在爱心中心附近random.uniform(0, 2*3.14)保证方向均匀。这个组合让粒子看起来是“从爱心内部涌出”而不是从轮廓线上喷出——如果你想要后者得把生成点改成沿贝塞尔曲线采样。def append_particles(particles, center, size): for i in range(15): # 每帧生成 15 个 r int(random.normalvariate(0, 10) * size) # 半径服从正态分布 angle random.uniform(0, 2 * 3.14) # 角度均匀分布 x center[0] r * math.sin(angle) # 极坐标转直角坐标 y center[1] r * math.cos(angle) particles.append(HeartParticle([x, y]))逻辑说明normalvariate(0, 10)的第二个参数是标准差10 意味着约 68% 的粒子落在半径 10 像素内视觉上就是中心密集、边缘稀疏。size参数在这里乘在半径上传 1 表示不缩放。注意原文draw_particles里对每个粒子调了两次p.draw(screen)——一次在循环内、一次在列表推导里这会导致每个粒子被画两遍、寿命被扣两次粒子消失速度翻倍。这是原代码的真实缺陷修法是把循环内的p.draw(screen)删掉只保留列表推导里的那次def draw_particles(screen, particles): for p in particles: p.move() # 只更新位置不重复绘制 return [p for p in particles if p.draw(screen)] # 绘制并过滤改完之后粒子存活时间恢复正常画面密度也会上来如果觉得太密就把append_particles的 15 降到 810。5. 避坑清单跑这份爱心代码时我踩过的五个坑现象窗口打开后全白看不到爱心也看不到粒子。原因screen.fill((255,255,255))在draw_heart之后执行把刚画的内容全盖了。原文顺序是先画粒子、再补粒子、再清屏、再画爱心清屏位置其实是对的但如果你手动调整过顺序很容易把 fill 挪到最后。 解决确认fill在draw_heart之前、pygame.display.flip()在最后顺序固定为“清屏 → 画爱心 → 刷新”。现象报TypeError: invalid color argument指向draw.circle那一行。原因pygame.draw.circle的颜色参数在 pygame 1.x 里不接受四元组带 alpha只有 2.x 的部分绘制函数支持。你装的如果是老版本(255, 0, 0, alpha)会直接报错。 解决pip install --upgrade pygame升到 2.x实在升不了就把 alpha 去掉改成三元组用半径随寿命缩小来模拟淡出。现象粒子全部堆在窗口左上角爱心也不在中间。原因append_particles传的中心是[400, 300]draw_heart传的是(400, 325)两者差了 25 像素更常见的是改了set_mode的窗口尺寸却没改这两个坐标。 解决把窗口尺寸、粒子中心、爱心圆心抽成三个常量放在文件顶部改一处全联动别让魔法数字散落在代码里。现象程序跑几秒后越来越卡风扇狂转。原因particles列表只增不减——draw_particles的返回值没被接住或者你误删了列表推导里的过滤条件死亡粒子永远留在列表里每帧都在遍历和绘制。 解决确保particles draw_particles(screen, particles)这行赋值存在且draw在lifespan为 0 时返回False。可以在循环里打印len(particles)观察是否稳定在几百而不是无限增长。现象爱心轮廓有缺口或线条错位。原因draw_heart里六组控制点的首尾没有严格衔接points1的终点(0, -0.05)和points2的起点相同但points3到points4的衔接依赖坐标完全一致手改任何一个数字都会撕开缺口。 解决改控制点时成对改——一组曲线的终点必须等于下一组的起点改完用size1小尺寸跑一遍缺口在低分辨率下最明显。6. 进阶改造把静态爱心变成可交互的粒子发射器跑通原版之后这份代码真正的价值在于它是个干净的起点。我一般会做三件事让爱心跟随鼠标、让粒子颜色随位置渐变、把帧率无关的运动补上。第一件最简单也最出效果——把draw_heart和append_particles的圆心从写死的(400, 325)换成pygame.mouse.get_pos()爱心就跟着鼠标走# 在主循环内、绘制之前获取鼠标位置 mx, my pygame.mouse.get_pos() # 粒子生成中心跟随鼠标爱心圆心同步偏移 append_particles(particles, [mx, my], 1) draw_heart(mx, my 25, 3) # 25 是原代码里圆心相对粒子中心的偏移量逻辑说明get_pos()返回的是窗口内坐标和set_mode的坐标系一致不需要转换。25这个偏移来自原代码draw_heart(400, 325)与append_particles(..., [400, 300])的差值保留它是为了让粒子和轮廓的相对位置不变。参数上鼠标移动快的时候粒子会拖尾想要更跟手就把每帧生成数从 15 提到 25代价是粒子总数上升。第二件是颜色渐变。原代码粒子固定红色改成按pos[1]映射色相从上到下红到粉观感会高级不少def draw(self, window): if not self.lifespan: return False alpha int(float(self.lifespan) / 50 * 255) # 用 y 坐标映射绿色通道越靠下越偏粉 g max(0, min(255, int(self.pos[1] / 600 * 180))) pygame.draw.circle(window, (255, g, 120, alpha), [int(self.pos[0]), int(self.pos[1])], 4) return True逻辑说明self.pos[1] / 600把纵坐标归一化到 01乘 180 限制绿色通道上限避免变成纯白。半径从 6 降到 4 是为了配合更丰富的颜色粒子小一点颜色层次才看得出来。这个改法不需要额外依赖纯算术。第三件是帧率无关运动属于“知道就行、不一定改”的范畴把move()里的位移乘以delta两帧间隔秒数速度单位从“像素/帧”变成“像素/秒”机器快慢不影响观感。代价是所有速度、加速度参数都要重新标定原代码的数值直接搬过去会慢得几乎不动。我的习惯是先用固定帧率把效果调满意再决定要不要上 delta——这份代码的定位是演示固定 40 帧足够。最后说个验证习惯每次改完参数我会先把clock.tick临时调到 10慢放看粒子轨迹是否连续、爱心轮廓有没有断点确认没问题再调回 40。慢放能暴露很多正常速度下看不见的坐标错位。从那以后我每次动贝塞尔控制点都强制先用 10 帧跑一遍再交付这个习惯帮我省了不少返工。希望这份拆解帮到你代码包拿去改改坏了原版还在。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询