
简介PDF格式的Python绘图代码讲解聚焦使用turtle模块绘制“小人发射爱心”的完整实现适合刚接触图形编程的初学者动手实践。内容详细拆解了函数定义、坐标定位、转向与画圆、填充颜色、文本输出等关键知识点并给出主程序调用逻辑帮助读者快速理解并复现整个绘图过程。资源包共1个文件为PDF文档大小约120KB轻量易读可随时查看。目前已有21122人学习下载是入门turtle绘图的热门参考。通过这份资料读者可以掌握自定义函数封装、参数传递、循环绘制复杂图形等技巧并学会将多个绘图动作组合成完整画面为后续创作更多趣味图形打下基础。1. 用Python画出小人发射爱心拆开看就是三次坐标运算用Python画出小人发射爱心整套实现只需要matplotlib和numpy两个库不依赖任何游戏引擎。爱心不是图片素材而是用参数方程逐点算出来的矢量路径小人也不是美术资源是圆形和矩形拼出来的几何角色。动画部分是每一帧给爱心坐标叠加位移增量。拆开就是三个知识点心形曲线的参数方程、用Patch对象拼角色、用FuncAnimation维护多物体的生命周期。这个例子适合刚学完Python语法的人练手也适合做数据可视化的工程师复习动画回调机制。网上常见的python爱心代码大多只画静态爱心这个版本带角色、带发射轨迹、还带连发逻辑。2. 先把爱心坐标算出来matplotlib画心形曲线的参数方程2.1 爱心公式怎么写16sin³t 与 13cost - 5cos2t - 2cos3t - cos4t画爱心最常用的方式是参数方程不是贴PNG素材。经典公式给出横纵坐标x 16 sin³ty 13 cos t - 5 cos 2t - 2 cos 3t - cos 4t。t 从 0 取到 2π每取一个 t 就算出一组 (x, y)把所有点连起来就是标准爱心轮廓。选这个公式而不是图片是因为它生成的坐标完全可控缩放、平移、旋转都只是对数组做数学运算。动画里要让爱心移动只需在 x、y 数组上叠加偏移量不需要重新加载任何素材如果用图片还要处理图像坐标系和子图坐标系的换算连发多颗时内存开销也大。有一个比例问题需要注意这个方程生成的爱心约占 17 个坐标单位宽、16 个单位高直接画会超出常见坐标轴范围所以代码里要手调 xlim、ylim。更重要的是设置 set_aspect(equal)强制两个坐标轴的尺度一致否则窗口一拉宽爱心就被压成横向椭圆。2.2 在matplotlib里画一条完整爱心路径的最小代码2.2.1 最小可运行代码import numpy as np import matplotlib.pyplot as plt t np.linspace(0, 2 * np.pi, 300) x 16 * np.sin(t) ** 3 y 13 * np.cos(t) - 5 * np.cos(2 * t) - 2 * np.cos(3 * t) - np.cos(4 * t) fig, ax plt.subplots(figsize(6, 5)) ax.plot(x, y, colorcrimson, linewidth3) ax.set_xlim(-20, 20) # 爱心宽17个单位留出余量 ax.set_ylim(-18, 18) ax.set_aspect(equal) # 保持横纵比例否则爱心变形 ax.axis(off) plt.show()逻辑说明np.linspace(0, 2 * np.pi, 300) 把 t 均匀切成 300 个点点数越多轮廓越光滑静态展示用 300做动画时降到 80~150 就够因为每帧都要重新算一遍坐标点数直接决定计算开销。x、y 两个数组分别保存爱心轮廓的横纵坐标ax.plot 把它们连成一条闭合曲线。set_aspect(equal) 是这里最关键的一行不加它改变窗口大小时爱心会被拉变形。2.2.2 三个影响显示效果的参数下表是静态和动画两种场景下的建议取值参数值不是越大越好要兼顾显示效果和计算性能。参数作用静态建议值动画建议值linspace 点数轮廓采样密度30080~150linewidth线条粗细31.5~2color颜色crimsonhotpink颜色这里多说一句动画里推荐亮色系hotpink、magenta 在浅色背景上对比度高crimson 偏暗多颗爱心叠在一起时容易糊成一片。2.3 为什么不直接用图片素材静态展示一张爱心 PNG用 plt.imshow 确实更快但到了动画环节就吃亏图片放大会模糊位移要处理 bounding box 和透明度合成连发多颗爱心时内存成倍上涨。参数方程是矢量路径每一帧只需要对 float 数组做加减乘除几十颗爱心同时存在也只是几十组数组在计算。这个选型也决定了后面的整体方案小人也用几何图形来拼画面里所有角色都是 matplotlib 的 artist 对象统一走“改坐标、重绘”一条路。3. 组装小人用Circle、Rectangle和Line2D拼出可复用角色3.1 小人的几何分解与坐标设计要把“发射”动作做出来小人要有两个特征整体面向发射方向手臂有明显抬起趋势。为此把小人拆成四类图形头部用 Circle身体用 Rectangle手臂和腿用 Line2D。所有图形的坐标都以脚底中心 (x, y) 为基准点偏移这样整体移动只需要改基准点不需要逐个调整。比例经过反复调整我用的是头部半径 0.9圆心在 (x, y2.4)身体宽 1.6、高 2.0左下角在 (x-0.8, y)两臂从肩部向外上方抬起腿从身体下方向两侧分开。拼出来的角色总高约 4.3 个单位和 16 个单位高的爱心站在一起时爱心显得大飞出去后的主体感更强。各部分关键参数可以对照下面这张表部位图形类型关键尺寸颜色头Circle半径 0.9#ffe0bd身体Rectangle宽 1.6、高 2.0#4a90d9手臂Line2D线宽 4#ffe0bd腿Line2D线宽 4#3333333.2 一个函数生成完整小人import matplotlib.patches as patches from matplotlib.lines import Line2D def build_person(ax, x0, y0): head patches.Circle((x, y 2.4), 0.9, color#ffe0bd) body patches.Rectangle((x - 0.8, y), 1.6, 2.0, color#4a90d9) arm_l Line2D([x - 0.8, x - 1.8], [y 1.8, y 1.4], color#ffe0bd, linewidth4) arm_r Line2D([x 0.8, x 1.8], [y 1.8, y 1.4], color#ffe0bd, linewidth4) leg_l Line2D([x - 0.4, x - 0.7], [y, y - 1.0], color#333333, linewidth4) leg_r Line2D([x 0.4, x 0.7], [y, y - 1.0], color#333333, linewidth4) for p in (head, body): ax.add_patch(p) for line in (arm_l, arm_r, leg_l, leg_r): ax.add_line(line) return [head, body, arm_l, arm_r, leg_l, leg_r]逻辑说明Circle 接收圆心坐标和半径Rectangle 接收左下角坐标、宽、高。Line2D 接收两个点的坐标每个点是一个 [x, y] 列表linewidth 控制线段粗细4 大约对应 4 个像素宽的线条画出来是明显的肉色手臂。函数返回六个 artist 对象组成的列表后面所有对小人状态的操作都通过这个列表进行坐标轴对象不需要放进列表因为它只需要添加一次。3.3 整体移动只改坐标不重建对象3.3.1 为什么不能每帧清空重画初学动画容易踩的坑是每帧调用 clear() 把整个 figure 清掉再重新画。这样确实能跑但画面会闪CPU 占用也高。正确做法是 artist 对象只创建一次每帧调用 set_center、set_xy、set_xdata、set_ydata 这类方法修改坐标matplotlib 在下一帧 draw 时按新坐标渲染画面才平滑。3.3.2 小人平移的封装函数def move_person(parts, dx, dy): head, body, arm_l, arm_r, leg_l, leg_r parts head.center (head.center[0] dx, head.center[1] dy) body.set_xy((body.get_x() dx, body.get_y() dy)) for line in (arm_l, arm_r, leg_l, leg_r): xd, yd line.get_xdata(), line.get_ydata() line.set_xdata(xd dx) line.set_ydata(yd dy)头、身体是 Patch用 set_center 和 set_xy四条线是 Line2D接口完全不一样只能用 get_xdata 读出旧坐标、加上增量、再 set_xdata 写回。如果不小心对 Line2D 调用 set_center直接报 AttributeError这是配错接口最典型的症状。另外注意 Rectangle 的 set_xy 接收的是左下角坐标不是中心坐标写代码前先算清楚。4. 用FuncAnimation驱动爱心飞行动画回调的完整写法4.1 update函数里每一帧要处理什么FuncAnimation 的核心是 update(frame) 回调frame 是自动递增的帧序号。每帧要做三件事判断是否到了发射时刻是就在小人位置生成一颗新爱心遍历所有在飞的爱心按速度更新坐标把飞出可视区的爱心从画布和列表里同时移除。下面代码假设 build_person 已经执行person_x、person_y 存的是小人基准点坐标。from matplotlib.animation import FuncAnimation hearts [] def update(frame): if frame % 12 0: t np.linspace(0, 2 * np.pi, 100) hx 16 * np.sin(t) ** 3 hy 13 * np.cos(t) - 5 * np.cos(2 * t) - 2 * np.cos(3 * t) - np.cos(4 * t) line, ax.plot(hx person_x, hy person_y, colorhotpink, linewidth2) hearts.append({line: line, age: 0}) for h in hearts[:]: h[age] 1 h[line].set_xdata(h[line].get_xdata() 0.8) h[line].set_ydata(h[line].get_ydata() 0.25) if h[age] 90: h[line].remove() hearts.remove(h) return [h[line] for h in hearts] anim FuncAnimation(fig, update, frames300, interval33, blitTrue) plt.show()逻辑说明frame % 12 0 控制发射节奏interval33 毫秒一帧也就是每 12 帧约 0.4 秒发一颗这是连发看起来连贯又不拥挤的节奏。每颗爱心生成后坐标就固化在 Line2D 对象里之后每帧对 x 加 0.8、对 y 加 0.25形成向右上的飞行轨迹0.8 和 0.25 的比值决定发射仰角。age 超过 90 帧时爱心已经飞出右侧可视区调用 remove() 删掉画布对象并移出列表防止绘制开销持续增长。两个细节最容易出错。遍历列表删除元素时必须用 hearts[:] 切片复制再遍历不能直接迭代 hearts否则删除时索引错位部分爱心会被跳过或重复处理。blitTrue 要求 update 返回所有变化过的 artist 对象这里返回所有在飞爱心的 line 引用漏返回会导致画面不刷新或抛出 RuntimeError。4.2 多颗爱心连发轨迹错开才像发射上面的代码连续发射有个问题所有爱心速度完全相同飞一会儿就排成一条直线看起来像一串珠子而不是发射效果。解决办法是每颗爱心生成时给一个随机的弹道系数让 y 方向增量在 0.2 到 0.3 之间浮动import random # 在发射分支里 direction random.uniform(0.8, 1.2) hearts.append({line: line, age: 0, dir: direction}) # 在更新循环里 h[line].set_ydata(h[line].get_ydata() 0.25 * h[dir])随机系数只在创建时取一次整段飞行过程中保持不变所以弹道是稳定的弧线不是抖动。想要更写实可以把 y 方向改成带初速和重力项的抛物线每颗爱心记录 vy每帧 vy - 0.02再 y vy。这样爱心先上升后下落画面丰富度会再上一个台阶。注意 age 阈值要跟着速度调整速度越快阈值越小否则爱心在屏幕外继续画几十帧纯属浪费。4.3 发射动画的参数对照表参数含义推荐值调节效果interval单帧间隔毫秒33越大越慢超过 50 明显卡顿frame % N发射频率12N 越小发射越密建议 8~16dx, dy 增量每帧位移0.8, 0.25dx/dy 比值决定弹道仰角age 阈值生命周期帧数90和速度联动保证及时清理linspace 点数爱心轮廓采样100低于 60 轮廓出现棱角这套参数针对 300 帧、30fps 的默认场景。如果画面里同时存在 10 颗以上爱心且帧率下降优先降低 linspace 点数和 age 阈值这两个对性能影响最大。想让爱心飞得更高不需要改速度直接调小 dy 或增大 dx弹道仰角会明显变化。5. 导出GIF与MP4pillow、ffmpeg和三个常见报错5.1 导出GIF和MP4的两条save命令动画在 plt.show() 里播放正常后导出是独立的坑。GIF 默认依赖 PillowMP4 依赖 ffmpeg两者都不是 matplotlib 自带的。pip install pillowanim.save(heart_shoot.gif, writerpillow, fps30) anim.save(heart_shoot.mp4, writerffmpeg, fps30)Pillow 装完 GIF 直接可存。MP4 报 MovieWriter ffmpeg unavailable说明系统里没有 ffmpeg 可执行文件或它不在 PATH。最快的处理是安装 ffmpeg 并把 bin 目录加进系统环境变量重启终端再执行不想动环境的话pip install imageio-ffmpeg再把 writer 换成该模块提供的可执行文件路径。在 VS Code 里跑这套代码时要先确认左下角选择的解释器和 pip install 装进的是同一个环境否则会一直报模块找不到。5.2 三个最高频报错的排查表报错信息原因处理方式ModuleNotFoundError: No module named PIL没装 Pillowpip install pillowMovieWriter ffmpeg unavailableffmpeg 不在 PATH安装 ffmpeg 配置 PATH 或用 imageio-ffmpegAttributeError: Line2D object has no attribute set_center把 Patch 接口用在 Line 上Line2D 用 set_xdata/set_ydataPatch 用 set_center/set_xy最后一个报错是混用接口导致的Line2D 和 Patch 是两个不同的类方法不能互换。记住这个边界排查这类动画代码能快很多。5.3 让爱心在飞行中旋转的进阶技巧直线飞行的爱心看久了有点呆加旋转后质感明显提升。原理是每帧先用旋转矩阵重算爱心点坐标再叠加这颗爱心的初始位置def rotate_points(hx, hy, angle): c, s np.cos(angle), np.sin(angle) return hx * c - hy * s, hx * s hy * c # 在更新循环里 h[age] 1 rot_x, rot_y rotate_points(hx0, hy0, h[age] * 0.05) h[line].set_xdata(rot_x h[base_x]) h[line].set_ydata(rot_y h[base_y])hx0、hy0 是创建这颗爱心时保存的原始轮廓坐标base_x、base_y 是它的发射位置。旋转角随 age 线性增长0.05 弧度每帧约 125 帧转一整圈视觉上是自然翻滚的飞行状态。系数取负值则反向旋转配合 4.2 节的随机弹道十几颗爱心几乎不会出现两两重合的画面。本文还有配套的精品资源点击获取