ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Python turtle打造满天烟花表白动画:粒子系统与完整代码

用Python turtle打造满天烟花表白动画:粒子系统与完整代码 前几天一个朋友突然找我说想要一份“能拿得出手”的Python表白代码。他说不想用那种弹几个对话框就完事的手段想找一个有画面、有情绪、能让人多看一会儿的效果。我翻了翻自己以前写的东西最后给他改出来一个满天烟花的版本深蓝色夜空里星星慢慢亮起月亮挂在右上方城市楼宇的剪影在底下安静地排开烟花从楼顶冲上去炸开所有颜色落下来之后屏幕上缓缓浮现出一句话——“星光月夜烟花皆归你我也归你”。这套效果完全用Python自带的标准库turtle实现不需要装任何第三方依赖代码量也不大新手复制下来就能跑。如果你也想给喜欢的人准备一个小小的惊喜或者只是想学一学turtle动画怎么写这篇东西可以直接抄作业。文章里除了完整代码我还会把烟花粒子、文字时机、中文字体这些容易踩坑的细节全部展开讲最后附带一个点击屏幕开出玫瑰的番外。1. 星光月夜烟花表白整体设计与技术选型1.1 表白场景需要的不是代码是氛围写这类程序之前先想清楚一个问题对方看到画面的时候情绪应该怎么走模式不是“哇好厉害”而是“哇好美”然后等文字出现的时候心里一暖。所以程序的功能排序应该是氛围大于特效节奏大于数量。很多人写烟花代码喜欢一上来就让屏幕炸成一片几十朵烟花同时开看起来确实热闹但也确实会审美疲劳。我的做法是把它设计成一条时间线前两秒先让星星、月亮、城市剪影把场景铺好然后零星放几发烟花等观众适应了夜空之后再让烟花陆陆续续开起来最后在所有烟火慢慢落尽的时候让那句文案浮现出来。整个过程大概十几秒情绪是从安静到热烈再回到安静。说白了这跟一场小型舞台演出的逻辑一模一样要有起承转合不能一上来就是高潮。另一个容易被忽略的是“最后停留的时间”。文案出现之后不要立刻关窗口也不要马上接新特效留出几秒钟空白让那句“我也归你”能砸进脑子里。我就是在这个间隙里放了“点一下屏幕送你一朵玫瑰”的提示把观众从被打动的情绪里轻轻拉回来再给一个彩蛋情绪又被往上推了一层。这种节奏安排比任何单点特效都重要。1.2 为什么用turtle而不是pygame、tkinter、HTML动手之前我其实犹豫过方案。这个项目可以用很多方式实现但每个方案都有自己的取舍我把它们列出来对比过方案依赖上手难度动画表现适合场景turtle标准库无需安装低中适合粒子和小图形教学、轻量表白动画tkinter Canvas无需安装中较高能扛更多节点桌面小应用、交互动画pygamepip安装中偏高高适合游戏级粒子复杂特效、需要音效HTML/CSS/JS需浏览器中极高可3D可交互Web端分享链接最后选了turtle原因很直接它满足“Python表白代码”这个关键词的第一直觉代码写起来像在画布上放海龟每个图形都是一个独立对象理解起来没有门槛。虽然性能不如pygame但满天烟花这种粒子量的动画turtle完全扛得住。pygame还要考虑安装环境、打包体积反而把简单的事情搞复杂了。当然turtle也有短板最明显的是没有真正的透明度渐变和3D能力但表白场景不需要这些。做技术选型的时候我一直坚持一个原则够用就行能把情绪表达出来的工具就是好工具而不是参数越复杂越厉害。1.3 画面布局与播放节奏一镜到底的浪漫整个画面我把它分成五个层这样代码结构才会清晰。第一层是夜空背景用一个深蓝黑色铺底不要用纯黑纯黑会显得死板深蓝黑才有夜空的通透感。第二层是星空一百二十颗左右的小亮点随机铺开大小不一颜色带一点米白和淡蓝模仿真实星空的色温差异。第三层是月亮和城市剪影月亮放在右上方城市剪影用一排高低错落的矩形拼出来压在画面底部给烟花一个“发射平台”。第四层才是动态的烟花层这一层由火箭和粒子组成每一帧都在变化。第五层是文字层也是最关键的一层。为了让文字出现得更庄重我把它放在烟花高潮回落之后不会和烟花抢视觉焦点反而像是一场表演的最后一句台词。播放节奏上我控制大约0.45到1秒发射一发新烟花同时限制屏幕上最多3-4发火箭避免粒子数量失控。烟花高度也做了随机有些在低空炸开有些升到接近月亮的高度形成错落感。这些细节单独看都不起眼叠在一起才会让整个画面“活”起来。2. 满天烟花的核心原理与参数细节2.1 烟花就是一个“抛沙子”粒子系统解释烟花之前先做一个类比你抓一把沙子往空中用力一抛每一粒沙子都会有一个初速度方向然后受到重力往下掉最后散落一地。烟花爆炸的过程跟这个几乎一模一样只不过把沙子换成了发光的点把地面换成了夜空。所以代码里真正干活的不是一朵朵“烟花”而是一群Particle对象。每个粒子都记录五个基本属性x坐标、y坐标、水平方向速度vx、垂直方向速度vy、剩余寿命life。这些属性决定了粒子每一帧会出现在哪里、往哪个方向飞、什么时候消失。配套的还有一个Rocket类负责模拟烟花弹上升的过程。烟花弹从城市剪影附近出发带着一个随机目标高度向上飞到达预定位置之后就把自己“炸掉”瞬间生成几十上百个粒子。粒子的初速度方向随机分布在圆周上速度大小在一个区间里随机这样炸出来的形状才是自然的圆形而不是定向的“扇形”或“直线”。我在Particle里加了一个小优化用__slots__声明属性这样每个粒子对象占用内存更少上千个粒子同时存活时性能会好一些。虽然这个项目规模不大但这种习惯对写过游戏逻辑的人来说很自然顺手就写了。2.2 关键参数怎么调才自然粒子系统能不能做出“像烟花”的感觉全看参数。我用一张表把最容易调崩的几个参数列出来附上我实测下来的建议值和使用逻辑参数建议值作用调大/调小影响火箭上升速度11像素/帧控制烟花弹升空节奏太大像窜天猴太小还没炸就力竭粒子初速度1.57.0控制爆炸扩散半径太大成花瓣四处飞太小挤成一团重力加速度0.12控制粒子回落速度太大会迅速坠地太小会悬浮不真实空气阻力系数0.98让粒子慢慢减速太大粒子很快停在半空太小像永动机粒子寿命0.81.6秒控制粒子存活时间太短看不到形状太长烟花尾巴拖沓单朵粒子数70110决定爆炸密度太多卡顿太少稀稀拉拉说一个最常见的坑有人为了让烟花更“爆”把粒子初速度调到10以上结果炸出来的效果像一簇羽毛还没看到形状就飞出了屏幕。烟花的观感重点不在“飞多远”而在于“炸开后受重力慢慢坠落”的轨迹所以重力系数和空气阻力往往比初速度更影响观感。我这里的数值是反复试出来的在不同电脑上可能有点差异但基本框架可以直接抄。颜色方面我准备了五组色盘每组3-4个颜色接近的色号。每朵烟花爆炸前随机选一组色盘粒子都从这组里取色这样同一朵烟花内部颜色和谐不同烟花之间又能拉开差异。如果所有的粒子都从全部颜色里随机抽屏幕上就会变成五颜六色的麻点反而不好看。2.3 星空、月亮、城市剪影的绘制小技巧静态背景层的绘制有个共同思路能用dot就不用circle。turtle.dot(size, color)可以直接在以当前坐标为圆心的位置画实心圆比circle再填色少操心很多尤其是星星这种几十上百个的小圆点用dot一行代码就搞定了。星空我会随机生成一百多个点坐标范围就是窗口宽高大小控制在2到5像素之间颜色从白、米白、淡蓝里随机选。这里有个小细节星星不要平均铺满整个屏幕上半部分密一点下半部分疏一点更符合人眼看夜空的习惯。月亮用两个dot做月牙效果。先画一个直径90像素的淡黄色圆然后把海龟向左上挪一点用背景色画一个直径80像素的圆盖上去重合的部分就被“挖”掉了形成月牙。这个做法比画圆弧简单很多还不会因为角度计算出错导致月牙变形。城市剪影则是用while循环生成一排随机的矩形高度在50到200像素之间宽度在40到120像素之间最后统一填充一个暗色。因为剪影颜色和夜空颜色接近会自然形成“远景里有一排建筑”的纵深感。3. 完整可运行的Python表白代码3.1 完整代码先放这里下面这段代码就是完整版Python 3.7及以上版本都能跑不需要安装任何第三方库。运行后能看到星空、月亮、城市剪影、满天烟花和“星光月夜烟花皆归你我也归你”的文案。代码里我用的是Windows下的中文字体Microsoft YaHeimacOS或Linux用户需要把CN_FONT换成本机系统中文字体这点在第4章的常见问题里会单独说。import turtle import random import math import time import sys # ---------- 基础设置 ---------- screen turtle.Screen() screen.bgcolor(#0a0a1a) screen.title(星光月夜烟花皆归你我也归你) screen.setup(1.0, 1.0) screen.tracer(0, 0) W screen.window_width() H screen.window_height() # 根据系统选择中文字体 if sys.platform.startswith(win): CN_FONT Microsoft YaHei elif sys.platform darwin: CN_FONT PingFang SC else: CN_FONT WenQuanYi Micro Hei # ---------- 星空 ---------- for _ in range(120): star turtle.Turtle() star.hideturtle() star.penup() star.speed(0) star.goto(random.randint(-W // 2, W // 2), random.randint(-H // 2, H // 2)) star.dot(random.randint(2, 5), random.choice([#ffffff, #fff8dc, #add8e6])) # ---------- 月亮满月 月牙覆盖 ---------- moon turtle.Turtle() moon.hideturtle() moon.penup() moon.goto(160, 210) moon.dot(90, #f5f5dc) moon.goto(148, 222) moon.dot(78, #0a0a1a) # ---------- 城市剪影 ---------- city turtle.Turtle() city.hideturtle() city.penup() city.goto(-W // 2, -H // 2) city.color(#111122) city.begin_fill() x -W // 2 while x W // 2: h random.randint(50, 200) w random.randint(40, 120) city.goto(x, -H // 2) city.goto(x, -H // 2 h) city.goto(x w, -H // 2 h) x w city.goto(W // 2, -H // 2) city.end_fill() # ---------- 粒子系统 ---------- class Particle: __slots__ (x, y, vx, vy, color, size, life) def __init__(self, x, y, color): self.x x self.y y self.color color self.size random.randint(4, 7) angle random.uniform(0, 2 * math.pi) speed random.uniform(1.5, 7.0) self.vx speed * math.cos(angle) self.vy speed * math.sin(angle) self.life random.uniform(0.8, 1.6) def update(self): self.vy - 0.12 # 重力 self.vx * 0.98 # 空气阻力 self.vy * 0.98 self.x self.vx self.y self.vy self.life - 0.02 return self.life 0 class Rocket: __slots__ (x, y, target_y, palette) def __init__(self): self.x random.randint(-W // 2 120, W // 2 - 120) self.y -H // 2 40 self.target_y random.randint(H // 4, H // 2) self.palette random.choice([ [#ff4d4d, #ffa500, #ffd700, #ffcccb], [#00ffff, #00bfff, #f0f8ff, #ffffff], [#ff69b4, #ff1493, #ffb6c1, #ffe4e1], [#ffd700, #ffa500, #fff8dc, #ffffe0], [#00ff7f, #7fff00, #adff2f, #f0fff0], ]) def update(self): self.y 11 return self.y self.target_y # ---------- 绘制画笔 ---------- pen turtle.Turtle() pen.hideturtle() pen.penup() pen.speed(0) rockets [] particles [] frame 0 text_shown False next_launch 10 def explode(rocket): for _ in range(random.randint(70, 110)): particles.append( Particle(rocket.x, rocket.y, random.choice(rocket.palette)) ) def draw(): pen.clear() for r in rockets: pen.goto(r.x, r.y) pen.dot(5, #e0e0e0) for p in particles: pen.goto(p.x, p.y) pen.dot(p.size, p.color) screen.update() def show_text(): t1 turtle.Turtle() t1.hideturtle() t1.penup() t1.goto(0, 90) t1.color(#ffd700) t1.write(星光月夜烟花皆归你, aligncenter, font(CN_FONT, 28, bold)) t2 turtle.Turtle() t2.hideturtle() t2.penup() t2.goto(0, 35) t2.color(#ff69b4) t2.write(我也归你, aligncenter, font(CN_FONT, 36, bold)) # 先放一发开场烟花 rockets.append(Rocket()) # ---------- 主循环 ---------- try: while True: if frame next_launch and len(rockets) 3: rockets.append(Rocket()) next_launch frame random.randint(25, 60) for r in rockets[:]: if r.update(): explode(r) rockets.remove(r) for p in particles[:]: if p.update(): particles.remove(p) draw() frame 1 if frame 420 and not text_shown: text_shown True show_text() time.sleep(0.02) except turtle.Terminator: pass3.2 代码关键部分逐段解读这段代码里最值得展开讲的是turtle.tracer(0, 0)和pen.clear()这一对组合。tracer(0, 0)的意思是关闭turtle的自动刷新所有图形变化都不会立刻渲染到窗口上必须手动调用screen.update()才统一刷新一遍。为什么要这么做因为烟花的粒子每帧都在移动如果每个粒子的移动都触发一次窗口刷新画面会疯狂闪烁而且速度慢到没法看。关闭自动刷新之后程序先把这一帧所有粒子都画到pen上再一次性update画面就干净流畅了。很多教程里给每个粒子建一个独立的Turtle对象粒子一多就卡。我让所有粒子共用一支叫pen的画笔每帧开始时pen.clear()把上一帧的动态内容全部清掉然后重新画所有粒子的当前位置。静态层不受影响因为星星、月亮、城市剪影分别是独立Turtle对象pen.clear()不会清掉它们。主循环里的try...except turtle.Terminator也是容易被忽略的地方。while True死循环在用户手动关闭窗口时会抛出Terminator异常如果不捕获程序会以一堆红色报错结束观感很差。包一层异常处理后用户点关闭窗口程序就安静退出不会弹错误信息。3.3 文案与提示语怎么改、什么时候出现文案出现的时机是frame 420也就是主循环跑了420帧。由于每帧末尾有time.sleep(0.02)动画大约每秒50帧所以420帧差不多对应8秒左右。这个时间点选在烟花已经放了几朵、又渐渐收尾的时候。如果你想更快看到文字把数字改成300甚至240想多铺垫一会儿改成600也完全可以。两行文字的位置分别是(0, 90)和(0, 35)一个偏上、一个在下方视觉上形成“主标题副标题”的层次。字号我用了28和36第二行“我也归你”更大因为它才是整个动画真正的情感落点。如果你想改成“生日快乐”或者“纪念日快乐”只需要替换write里的字符串位置和颜色都可以保留。颜色上第一行用金色#ffd700第二行用粉红#ff69b4在深蓝黑背景上对比度很舒服也不会刺眼。4. 常见问题与排查技巧4.1 窗口闪退、运行报错怎么办闪退是最常见的现象尤其是双击.py文件运行的新手用户。原因一般是两个一是电脑里压根没装Python或者只装了Microsoft Store里那个无效的占位版二是脚本运行过程中发生了异常导致窗口直接退出。最简单的判断方法是打开命令行手动执行python 表白.py如果报错错误信息会留在终端里而不是一闪而过。我建议先把代码简化到最小可运行状态做验证比如只写三行import turtle turtle.forward(100) turtle.done()这个能跑通再把完整代码贴进去。如果连这个都报错说明Python环境没装好。如果最小脚本能跑但完整脚本闪退就把完整脚本里的try...except turtle.Terminator临时去掉让报错信息显示出来基本能立刻定位问题。4.2 中文显示成方块怎么解决中文变成方块的根源是turtle底层用的Tk字体找不到你指定的中文字体而不是代码逻辑有错。我代码里已经根据操作系统做了判断Windows用Microsoft YaHeimacOS用PingFang SCLinux用WenQuanYi Micro Hei。但总有一些精简版系统没装这些字体尤其是某些Windows精简版和Linux服务器版中文字体有缺失。实在不行有几个备选方案一是把字体名改成本机确实存在的字体Windows可以试试SimHei或SimSun二是把中文文案换成英文字符串比如“Starlight, Moonlight, Fireworks, All for You”这种字体是绝大多数系统自带的三是如果一定想保留中文可以在Windows的“设置-个性化-字体”里安装新的中文字体安装后重启程序就能识别。这三种方法里换英文字符串是最快的兜底方案效果也不差。4.3 动画卡顿或粒子糊成一团卡顿基本是粒子数量超出电脑渲染能力。我把单朵烟花粒子数控制在70到110同时限制火箭最多3发在这个配置下普通办公本都能流畅跑。如果你发现明显卡顿优先做两件事把random.randint(70, 110)改成random.randint(50, 70)把len(rockets) 3改成len(rockets) 2。这两个改动会让画面稍微稀疏一点但流畅度提升非常明显。粒子“糊成一团”是另一个常见现象原因是粒子画得太密或者上一帧的残影太明显。解决方法是把粒子的size从random.randint(4, 7)改成random.randint(3, 5)同时把粒子寿命从random.uniform(0.8, 1.6)缩短到random.uniform(0.6, 1.2)让粒子更快消散。还有一点屏幕分辨率太高也会增加渲染压力如果卡顿严重把screen.setup(1.0, 1.0)改成固定尺寸比如screen.setup(1280, 800)观感反而更细腻。4.4 想把程序打包成exe发给她有哪些坑打包exe是很多人问的高频问题我的建议是优先用源码打包只作为最后的兜底。真需要打包用PyInstaller是最成熟的方案命令行这样写pip install pyinstaller pyinstaller -F -w 表白.py-F是打包成单个exe-w是隐藏Python的控制台黑窗口避免运行时候跳出一个命令提示符观感不好。打包完成后exe文件在dist目录下。这里有几个绕不开的坑。第一个是turtle打包后可能报错找不到Tcl/Tk解决办法是在命令行加上--hidden-import_tkinter如果还不行就需要检查PyInstaller版本新版一般能自动带上tcl和tk文件。第二个是中文文案在对方电脑上显示成方块因为这个exe依赖对方系统字体。第三个是某些杀毒软件会把PyInstaller生成的单文件exe误报这属于误报加白名单就行。但说实话打包exe之后文件体积通常在15MB以上启动速度也比源码慢如果对方电脑上装了Python直接发源码文件反而是最好的体验。4.5 常见问题速查表症状可能原因解决方案运行后窗口一闪而过Python环境或脚本异常命令行手动运行查看报错中文显示为方块系统缺少中文字体换字体或切换为英文字符串动画严重卡顿粒子数量过多减少粒子数和同时火箭数粒子糊成一团粒子过大或残影重调小尺寸、缩短寿命窗口关闭后报错未捕获Terminator用try/except包住主循环打包exe无法运行缺少Tcl/Tk依赖加--hidden-import_tkinterLinux下无法运行缺少tkinter图形库安装python3-tk系统包5. 番外玫瑰与表白效果升级5.1 玫瑰番外的两种画法挑一个顺手的玫瑰番外我提供两种实现第一种是极坐标玫瑰线代码非常短只有十几行效果是数学意义上的“玫瑰曲线”花瓣对称、整齐多了一丝理工科的浪漫def draw_math_rose(center_x0, center_y-50): t turtle.Turtle() t.hideturtle() t.penup() t.color(#ff2d55) t.begin_fill() for i in range(0, 720, 2): rad math.radians(i) r 60 * math.cos(3 * rad) # 三瓣玫瑰线 x center_x r * math.cos(rad) y center_y r * math.sin(rad) t.goto(x, y) t.pendown() t.end_fill()第二种是更接近大众认知的“玫瑰花”用爱心形状做花朵再配上绿色枝干和叶子。一颗红心其实就是两次对称的圆弧用turtle画出来有手绘感放到表白场景里比数学曲线更有温度。我用的是下面这段def draw_rose(): t turtle.Turtle() t.hideturtle() t.speed(6) # 红色爱心花朵 t.penup() t.goto(0, -50) t.pendown() t.color(#ff2d55) t.begin_fill() t.left(50) t.circle(60, 200) t.right(100) t.circle(60, 200) t.end_fill() # 绿色枝干 t.penup() t.goto(0, -50) t.pendown() t.color(#228b22) t.setheading(270) t.forward(150) # 左边叶子 t.left(30) t.forward(45) t.backward(45) # 右边叶子 t.right(60) t.forward(45)我个人更建议用第二种接受度更高看起来也更像一个“彩蛋”。如果想让玫瑰再精致一点可以在爱心内部用淡粉色画几个小圆弧模拟花瓣层次不过对表白场景来说一个干净利落的红心加绿叶已经足够。重要的是心意不是复杂度。5.2 让玫瑰“藏”在最后彩蛋触发设计玫瑰不该抢主程序的戏最好让它像一个隐藏彩蛋需要观众主动触发。代码里可以用screen.onscreenclick绑定鼠标点击事件点击屏幕任意位置才画出玫瑰。最关键的一步是加一个rose_drawn标志位防止每次点击都重复画一朵那样就变成刷屏了。实际代码可以这样写rose_drawn False def show_rose(x, y): global rose_drawn if rose_drawn: return rose_drawn True draw_rose() screen.onscreenclick(show_rose)这段逻辑要加到主循环开始之前而screen.onscreenclick本身不阻塞主循环所以烟花动画会继续播放。我在实际运行里发现一个小问题画玫瑰的turtle如果速度太慢会显得很生硬所以draw_rose函数里把speed(6)设得比较高。另外玫瑰画完后记得hideturtle()把画笔隐藏避免出现一个箭头标记破坏画面。“点一下屏幕”这个交互隐藏得恰到好处文案出现后观众会因为好奇去点屏幕玫瑰作为最后的惊喜跳出来整个过程有互动、有反馈比纯播放动画更打动人。5.3 表白现场实操的四个建议代码写完只是一半现场演示才是真正的考验。我说几个自己踩过的坑和观察到的细节。第一正式使用前至少完整跑三遍原代码。不要改一行参数就仓促上场烟花动画最怕运行到中间弹个异常气氛全毁。最好设成和正式环境一样的窗口尺寸不要出现“在家里全屏正常到对方电脑上窗口缩小或放大了”的情况。第二关闭系统通知和弹窗。Windows下右下角的微信、QQ弹窗、邮件提醒都是破坏氛围的“定时炸弹”。演讲者模式、勿扰模式能开就开。第三准备背景音乐。代码没有内置音效纯画面缺一点氛围。我的实际经验是在烟花升起时放比较轻的纯音乐文案出现时把音量稍微调小会形成一种“整个世界安静下来”的感觉。音乐用手机外放或蓝牙音箱都行注意别抢走屏幕画面的注意力。第四文案一定要定制。把“星光月夜烟花皆归你我也归你”改成你们之间的暗号、昵称、纪念日日期效果会完全不同。程序只是传递情绪的工具真正被记住的永远是那句只属于你们两个人的话。我在实际测试的时候最打动我的其实不是满天烟花而是最后那句“我也归你”出现后屏幕短暂的安静。代码本身不复杂复杂的是愿意花一下午调试参数、反复跑循环、只为十几秒画面的人。如果这份代码最后也能帮你把想说的话说出来那我写这篇文章的目的就算达到了。
返回列表