1. 从“李峋同款”说起爱心代码飘散效果到底是个什么东西如果你最近刷到过《点燃我温暖你》里李峋那段在命令行里敲出跳动的爱心大概率会跟我一样第一反应是“这玩意儿我也得整一个”。但真去搜“爱心代码”的时候你会发现满屏都是静态的、单调的、只有一颗心的版本看两眼就腻了。而“比李峋还要好看的爱心代码飘散效果”核心差异就在“飘散”两个字上——它不是一颗心杵在那儿而是大量爱心粒子从中心向外扩散、飘落、旋转配合颜色渐变和拖尾形成一种类似粒子烟花或者花瓣雨的效果。这个项目的本质是用代码生成一个动态粒子系统每个粒子是一颗爱心形状粒子在生命周期内完成“生成—扩散—衰减—消失”的完整过程。它解决的是“静态爱心太单调、动态爱心太机械”的问题适合所有想用代码做点浪漫小玩意儿的人不管你是刚学编程的新手还是想找个练手项目的进阶选手。Python、C语言、HTML都能实现区别只在于渲染方式和性能上限。我前后用Python的turtle和pygame各写过一版又用HTML5 Canvas重写了一版最后还拿C语言在控制台里做了个字符版的。踩过的坑包括但不限于粒子数量一多就卡成PPT、爱心形状画出来像屁股、颜色渐变算错导致一片灰、飘散方向随机性太差看起来像爆炸而不是飘散。下面我把整个思路、实现细节和避坑经验完整拆一遍你照着抄作业就行。2. 整体设计思路为什么是粒子系统而不是单颗心2.1 单颗爱心的局限性在哪里先说说为什么不能直接画一颗心然后让它动。最常见的做法是用数学公式画心形线比如参数方程 x 16sin³ty 13cos t - 5cos 2t - 2cos 3t - cos 4t这个公式画出来的心形确实标准但问题在于它是一整条闭合曲线你没法单独控制每个部分的运动。想让心“飘散”就得把心拆成无数个小点每个点独立运动这就是粒子系统的思路。另一个问题是视觉层次。单颗心不管怎么加渐变、加光晕它始终是一个平面图形缺少纵深感和随机性。而粒子系统天然带有随机属性——每个粒子的初始位置、速度、大小、透明度都可以不同叠加起来就有一种“活的”感觉。你可以理解为单颗心是印刷海报粒子爱心是烟花。2.2 粒子系统的核心要素拆解一个完整的爱心飘散粒子系统需要定义清楚这几个东西粒子发射器决定粒子从哪里冒出来。常见做法是从心形线上的随机点发射或者从中心区域按心形分布发射。粒子属性每个粒子要有位置、速度、加速度、大小、颜色、透明度、旋转角度、生命周期。这些属性在初始化时随机赋值在每一帧更新。运动规则粒子受什么力影响。最简单的就是给一个向外的初速度加上重力和空气阻力让它先扩散再缓慢下落。渲染方式每个粒子画成什么形状。可以是一个小圆点、一个小爱心、一个字符或者一张贴图。生命周期管理粒子从出生到消失的时间以及透明度随时间的衰减曲线。我试过几种不同的参数组合最后发现“初速度向外轻微重力透明度线性衰减”这个组合最像“飘散”而不是“爆炸”。初速度如果太大粒子瞬间飞出去就没了重力如果太大粒子还没飘起来就往下掉透明度衰减如果太陡粒子刚出来就消失看起来稀稀拉拉。2.3 三种技术路线的选型对比不同语言和框架实现这个效果差异挺大的。我整理了一个对比表方便你根据自己的情况选技术路线渲染方式性能上限上手难度适合场景Python turtle矢量绘图低200粒子左右极低新手练习、截图分享Python pygame像素绘制中1000粒子左右低桌面动态壁纸、录屏HTML5 Canvas像素绘制高5000粒子以上中网页展示、手机也能跑C语言 控制台字符输出极低100字符左右中终端炫技、嵌入式C语言 EasyX像素绘制中高中高Windows桌面程序如果你只是想快速看到效果Python的turtle最省事但性能确实拉胯。想要流畅的飘散效果HTML5 Canvas是首选浏览器直接打开就能跑手机也不卡。C语言控制台版虽然性能差但那种字符跳动的复古感是别的方案给不了的。3. 核心细节解析爱心形状怎么画才不像屁股3.1 心形线的数学原理与参数选择画爱心最常用的就是那个参数方程但很多人直接抄过来发现画出来的心要么太胖要么太尖。问题出在参数范围上。标准方程是x 16 * sin(t)^3 y 13 * cos(t) - 5 * cos(2t) - 2 * cos(3t) - cos(4t)t 的范围是 0 到 2π。这个方程画出来的心宽度是32x从-16到16高度大约是30左右。如果你直接把它画在屏幕上会显得很扁。我的做法是给y轴乘一个缩放系数比如1.2到1.5之间让心看起来更修长。另外如果你想要那种“胖乎乎”的可爱风格可以把x轴的系数调小一点比如0.8。还有一个细节这个方程画出来的心底部是尖的顶部有两个圆弧。如果你把t的步长设得太大比如0.5画出来的心会有棱角。步长建议在0.01到0.05之间太小了计算量大太大了不够圆滑。我一般用0.02兼顾性能和精度。3.2 粒子初始位置的分布策略粒子从哪儿出发直接决定了飘散效果好不好看。我试过三种分布方式第一种是均匀随机分布在整个屏幕范围内随机撒点。这种做法简单但粒子跟爱心形状没关系看起来就是一堆乱飞的星星。第二种是心形线采样沿着心形线方程取点每个点作为一个粒子的初始位置。这样粒子一开始就组成一个爱心然后向外飘散。效果最直观但问题是粒子都挤在线上心形内部是空的看起来不够饱满。第三种是心形内部填充用拒绝采样法在矩形区域内随机撒点判断点是否在心形内部如果在就保留不在就丢弃。这样粒子均匀分布在整个心形区域飘散的时候有一种“心碎了”的感觉。我最后选的是第三种配合一个从中心向外的初速度效果最像“飘散”。判断点是否在心形内部可以用心形线的隐式方程或者简单点用射线法。我图省事直接用参数方程生成大量边界点然后对每个随机点判断它是否在边界点围成的多边形内。Python里用matplotlib的Path.contains_point就能搞定HTML里自己写个射线法也不复杂。3.3 颜色渐变与透明度衰减的计算颜色这块我踩过最大的坑就是直接用RGB随机。随机RGB出来的颜色又脏又乱完全没有浪漫感。正确的做法是用HSV色彩空间固定饱和度和明度只让色相在粉色到红色之间变化。比如色相从330度到360度饱和度0.8明度1.0出来的就是各种粉红、玫红、深红怎么搭都好看。透明度衰减我试过线性和指数两种。线性衰减就是 alpha 1 - t/lifetime简单直接但粒子消失的时候有点突然。指数衰减是 alpha exp(-3 * t/lifetime)前期衰减慢后期衰减快粒子会在空中停留更久然后快速消失。实测下来指数衰减更自然尤其是配合拖尾效果的时候。还有一个技巧给每个粒子的透明度加一点随机偏移比如 alpha * random(0.8, 1.0)。这样粒子不会同时消失而是一个接一个淡出层次感更强。3.4 飘散运动的物理模拟参数运动规则我调了很多版最后稳定下来的参数是这样的初速度方向从心形中心指向粒子位置大小在 2 到 5 像素/帧之间随机。重力每帧给 y 方向速度加 0.05 到 0.1。空气阻力每帧速度乘以 0.98 到 0.99。水平扰动每帧给 x 方向速度加一个 -0.02 到 0.02 的随机值。这套参数下来粒子会先向外扩散然后逐渐减速最后缓慢下落。水平扰动让粒子的轨迹不是直线而是有轻微的左右摇摆看起来更像花瓣飘落。如果你想要更“爆炸”的效果把初速度调到 8 到 10重力调到 0.2。如果你想要更“温柔”的效果初速度 1 到 2重力 0.02。注意重力不要设成每帧加固定值而是要用“速度 加速度”的方式。如果你直接改位置粒子会越飞越快看起来像被吸走而不是飘散。4. 实操过程从零写一个HTML5 Canvas版爱心飘散4.1 页面骨架与Canvas初始化HTML部分很简单一个全屏Canvas就行。关键是CSS要把body的margin和padding清零否则会有白边。Canvas的宽高用window.innerWidth和window.innerHeight并且监听resize事件动态调整。!DOCTYPE html html head style body { margin: 0; overflow: hidden; background: #0a0a1a; } canvas { display: block; } /style /head body canvas idc/canvas script const canvas document.getElementById(c); const ctx canvas.getContext(2d); let W, H; function resize() { W canvas.width window.innerWidth; H canvas.height window.innerHeight; } window.addEventListener(resize, resize); resize(); /script /body /html背景色我用的 #0a0a1a一种很深的蓝黑色比纯黑更有质感。如果你想要白天效果可以改成 #1a1a2e 或者直接白色背景配深色粒子。4.2 心形粒子生成器的完整实现先定义一个函数生成心形内部的随机点。我用的是参数方程加拒绝采样的混合方案function heartPoint() { // 在心形包围盒内随机取点 const x (Math.random() - 0.5) * 2.5; const y (Math.random() - 0.5) * 2.5; // 心形隐式方程判断 const a x * x y * y - 1; if (a * a * a - x * x * y * y * y 0) { return { x: x * 120, y: -y * 120 }; } return null; }这个隐式方程 (x²y²-1)³ - x²y³ 0 是心形线的另一种表达比参数方程更适合做内部判断。系数120是缩放你可以根据屏幕大小调整。注意y要取负因为Canvas的y轴向下。然后粒子类class Particle { constructor() { const p heartPoint(); this.x W / 2 (p ? p.x : 0); this.y H / 2 (p ? p.y : 0); const angle Math.atan2(this.y - H / 2, this.x - W / 2); const speed 2 Math.random() * 3; this.vx Math.cos(angle) * speed; this.vy Math.sin(angle) * speed; this.size 2 Math.random() * 4; this.hue 330 Math.random() * 30; this.alpha 1; this.life 1; this.decay 0.005 Math.random() * 0.01; } update() { this.vx * 0.99; this.vy * 0.99; this.vy 0.05; this.vx (Math.random() - 0.5) * 0.04; this.x this.vx; this.y this.vy; this.life - this.decay; this.alpha Math.exp(-3 * (1 - this.life)); } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fillStyle hsla(${this.hue}, 80%, 60%, ${this.alpha}); ctx.fill(); } }这里每个粒子画的是圆点如果你想让粒子本身也是爱心形状可以把arc换成画小爱心的路径但性能会下降不少。我的建议是粒子用圆点然后在中心位置画一颗大的爱心轮廓作为“发射源”视觉上更有层次。4.3 动画循环与粒子池管理主循环用requestAnimationFrame每帧清屏、更新粒子、绘制粒子。粒子池的管理很关键不能无限往数组里push否则内存会爆。我的做法是维护一个固定大小的数组粒子“死亡”后不删除而是重置属性重新利用。const particles []; const MAX_PARTICLES 2000; function init() { for (let i 0; i MAX_PARTICLES; i) { particles.push(new Particle()); } } function loop() { ctx.fillStyle rgba(10, 10, 26, 0.15); ctx.fillRect(0, 0, W, H); for (let p of particles) { p.update(); p.draw(); if (p.life 0) { Object.assign(p, new Particle()); } } requestAnimationFrame(loop); }注意清屏用的是半透明矩形而不是clearRect这样会留下拖尾效果粒子看起来有光晕。透明度0.15是我试出来的太低拖尾太长会糊成一片太高就没有拖尾感。4.4 性能调优与帧率控制2000个粒子在普通笔记本上能跑到60帧但如果你在手机上打开可能会掉到30帧以下。优化手段有几个减少粒子数量到1000视觉上差别不大。把arc换成fillRect画方块比画圆快很多。关闭拖尾用clearRect清屏。用离屏Canvas预渲染粒子贴图然后drawImage。我实测下来用fillRect画2x2的方块2000粒子在手机上也能跑满60帧。如果你追求圆形粒子可以用预渲染的圆形贴图性能介于arc和fillRect之间。提示不要每帧都创建新的Particle对象对象创建和垃圾回收的开销比你想象的大。用对象池复用是标准做法。5. 常见问题与排查技巧实录5.1 爱心画出来像屁股怎么办这是最常见的问题原因通常是y轴缩放不对。标准心形线方程画出来的心宽高比接近1:1但人眼觉得好看的心形宽高比应该在1:1.2到1:1.4之间。解决办法很简单把y坐标乘以1.3。另外心形顶部的两个圆弧如果太尖可以把参数方程里的cos项系数微调一下或者直接用贝塞尔曲线手绘一个心形路径。还有一个原因是颜色。如果你用纯红色填充看起来就像解剖图。换成粉红渐变加一点光晕立刻就不一样了。5.2 粒子飘散方向乱七八糟像爆炸问题出在初速度的方向上。如果你给每个粒子随机方向那肯定是爆炸。正确的做法是初速度方向从心形中心指向粒子当前位置这样粒子是沿着径向向外扩散的。然后再加一个很小的随机扰动让轨迹不那么死板。另外初速度的大小要跟粒子到中心的距离成正比。离中心近的粒子速度小一点离中心远的粒子速度大一点这样扩散的时候不会出现“内圈追外圈”的混乱感。5.3 颜色发灰、发暗、没有浪漫感RGB随机是万恶之源。一定要用HSL或者HSV固定S和L只变H。粉色系的H范围是330到360S在70%到90%L在50%到70%。如果你想要紫色调H可以到280到320。千万不要让L超过80%否则会变成白色也不要低于40%否则会变成暗红。还有一个技巧给粒子的颜色加一点随生命周期的变化。比如粒子刚出生时H是350快消失时H变成330这样整体色调会有一种流动感。5.4 粒子数量一多就卡顿性能问题一般出在三个地方绘制调用太多、对象创建太频繁、清屏方式不对。解决办法用对象池不要每帧new。用fillRect代替arc。用半透明fillRect代替clearRect做拖尾。如果还卡把粒子数量减半或者降低Canvas分辨率用CSS放大。我在一台2015年的MacBook Pro上测试2000个fillRect粒子能稳定60帧2000个arc粒子掉到45帧左右。差距还是很明显的。5.5 常见问题速查表问题现象可能原因解决方法爱心形状像屁股y轴缩放不足y坐标乘1.2到1.4粒子爆炸式飞散初速度方向随机改为径向向外颜色发灰发暗用了RGB随机改用HSL固定S和L粒子同时消失衰减参数相同给decay加随机偏移动画卡顿绘制调用过多用fillRect对象池拖尾太糊清屏透明度太低调到0.1到0.2之间粒子飞出屏幕不消失没有生命周期加life和decay心形内部太空只采样了边界用拒绝采样填充内部6. 进阶玩法让效果比李峋版更高级的几个思路6.1 多层粒子叠加与景深效果单层粒子看起来比较平如果你想要那种“漫天爱心”的纵深感可以加两层背景层粒子小、速度慢、透明度低前景层粒子大、速度快、透明度高。两层用不同的颜色范围背景偏紫前景偏粉。这样看起来就像有远近之分立体感立刻出来了。实现上就是维护两个粒子数组更新和绘制逻辑一样只是参数不同。性能开销翻倍但视觉效果提升很明显。6.2 爱心拖尾与光晕渲染拖尾有两种做法一种是用半透明清屏让上一帧的残影留下来另一种是给每个粒子记录历史位置画一条渐变的线。第一种简单第二种更可控。我一般用第一种配合粒子的透明度衰减效果已经够好了。光晕可以用shadowBlur但性能开销很大。替代方案是画一个比粒子大一圈的半透明圆颜色跟粒子一样但透明度减半。这样每个粒子实际上画两次但比shadowBlur快得多。6.3 交互响应鼠标跟随与点击爆发让爱心跟着鼠标走或者点击屏幕时从点击位置爆发一波爱心这种交互能大大提升趣味性。实现思路是监听mousemove和click事件把鼠标位置作为新的发射器位置或者把点击位置作为爆发中心。粒子生成时从这些位置取初始坐标。注意移动端要用touch事件并且要阻止默认的滚动行为否则页面会跟着手指滑动。6.4 文字嵌入与个性化定制如果你想把爱心飘散效果做成表白页面可以在中心位置叠加一行文字比如“I Love You”或者对方的名字。文字用CSS绝对定位放在Canvas上层加一点淡入淡出动画。粒子从文字周围飘散出来效果比单纯的心形更有意义。文字颜色建议用白色或者浅粉色加一点text-shadow跟背景形成对比。字体选圆润一点的比如Comic Sans MS或者手写体别用宋体。6.5 从Python到C语言的移植要点Python的turtle版移植到C语言控制台核心逻辑是一样的只是渲染方式从画点变成了打印字符。每个粒子对应一个字符位置用gotoxy函数移动光标然后打印一个心形符号或者星号。颜色用ANSI转义码控制比如\033[35m是紫色\033[0m是重置。C语言版的难点在于控制台刷新率有限粒子数量不能太多一般50到100个就够了。另外Windows和Linux的gotoxy实现不一样Windows用SetConsoleCursorPositionLinux用ANSI转义码\033[y;xH。如果你要跨平台可以用条件编译。Python的pygame版移植到C语言的EasyX主要是把pygame的draw.circle换成EasyX的fillcircle事件循环换成EasyX的消息循环。逻辑几乎不用改但EasyX只支持Windows跨平台还是得用SDL或者OpenGL。7. 我踩过的那些坑和最后的小技巧这个爱心飘散效果我前后写了四版从Python到HTML再到C语言每一版都踩了不同的坑。最早用turtle的时候粒子一多就卡死后来才知道turtle是单线程矢量绘图根本不适合做粒子系统。后来换pygame发现全屏刷新率上不去又去研究双缓冲和脏矩形更新。最后用HTML5 Canvas才算是找到了性能和效果的平衡点。有一个小技巧我一直在用给粒子的初始位置加一个“心形内部密度权重”。具体做法是在拒绝采样的时候如果点靠近心形中心就多保留几次如果靠近边缘就少保留几次。这样粒子在心形内部是中间密、边缘疏飘散的时候中间先散开边缘后散开层次感特别好。还有一个参数是粒子的旋转。如果你把粒子画成小爱心而不是圆点给每个粒子一个随机的旋转角度并且每帧旋转一点点看起来就像花瓣在风中翻转。旋转角度用Math.random() * Math.PI * 2初始化每帧加0.02到0.05。这个细节加上去之后整个效果的高级感会提升一个档次。最后说一个实际部署的经验如果你要把这个效果放到网页上记得加一个“点击开始”的遮罩层。因为很多浏览器会限制自动播放的动画尤其是移动端。用户点击之后再启动动画循环既符合规范又能让用户有一个“启动”的仪式感。遮罩层用半透明黑色中间放一个爱心图标和“点击查看”的文字点击后淡出动画开始。这个细节虽然小但体验上差别很大。