ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

matplotlib动画实战:FuncAnimation绘制小人发射爱心

2026/9/19 21:01:26 拓冰建站 浏览量
matplotlib动画实战:FuncAnimation绘制小人发射爱心 简介使用Python的turtle模块绘制“小人发射爱心”图形是这份PDF教程的核心内容。资源面向Python初学者与趣味编程爱好者通过一个完整可运行的示例演示了如何利用标准库turtle实现图形绘制从定义go_to、head、leg、hand、eye等函数到坐标定位、转向、画圆、填充颜色再到用big_Circle、small_Circle组合出爱心形状。PDF中还包含参数含义、速度与颜色设置、write文本绘制等知识点解析适合边读边练。整个资料包共1个文件为PDF文档体积约120KB轻量易用目前已有21122人学习浏览是入门turtle绘图的高人气参考。读者对照PDF即可复现代码并尝试修改坐标、大小、文案创造出个性化的表达图形。1. 用 Python 画出小人发射爱心先想清楚要画什么一个「小人发射爱心」的画面拆开看只有三件事一个能认出是人的轮廓、一颗形状标准的心、以及一段让它从人物身上飞向目标的运动轨迹。单独画这三样都不难难的是把时间和坐标统一起来——爱心从哪个点出发、飞多快、在哪儿消失这些全都由同一套坐标系和同一组帧参数决定。我见过不少初学者卡在最后一步静态图画得不错一加动画就东飘西散本质是没把「每一帧画面」当作函数返回值来设计。这篇文章用 matplotlib 的 FuncAnimation 串起整个过程给出从静态到动画的完整代码读完你拿到的是一套可以改人物位置、改爱心个数、改飞行路径的模板而不是一张死图。2. 坐标系与基础图形先把火柴人和爱心画进同一个画布2.1 为什么选 matplotlib 而不是 turtle 或 pygame画「小人发射爱心」这类图形可选的库其实不少。turtle 适合演示画笔运动但做「人物固定、物体发射」这种多对象场景时要反复提笔落笔坐标管理很别扭pygame 是游戏引擎功能强大但对一个纯图形动画来说引入事件循环、精灵类这些概念学习成本明显偏高。matplotlib 的优势在于它的 Artist 对象模型画布上的每个元素线条、圆形、多边形都是一个可独立修改的 Python 对象动画本质只是每帧去改这些对象的属性然后重绘。对「画小人 画爱心 让他们动起来」这个需求来说matplotlib 是性价比最高的选择而且后续要做数据可视化时学的这套逻辑完全能复用。2.2 定义坐标轴给所有元素一个共同的「世界」动手画之前先把坐标系定下来。我习惯用一个居中的坐标系原点放在人物脚下这样人物高度、爱心飞行轨迹都可以用相对坐标描述后续调参数不用改绝对值。import numpy as np import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation fig, ax plt.subplots(figsize(8, 6)) ax.set_xlim(-6, 6) ax.set_ylim(-1, 8) ax.set_aspect(equal) ax.axis(off)这段代码做的事情是建立一块 8:6 的画布横轴范围 -6 到 6竖轴范围 -1 到 8给爱心飞行留出上方空间。set_aspect(equal)是容易忽略但很重要的一行不设置它matplotlib 会自动缩放坐标轴让图形填满画布心形会被拉成椭圆圆头也会变成扁的。axis(off)去掉边框和刻度让画面只剩下图形本身。2.3 火柴人小人的管线式绘制「小人」不必追求精细火柴人足够。关键是把它拆成几个可复用的绘制步骤头、躯干、双手、双腿。每个部分都是一组坐标点用 Line2D 或 Polygon 画出来。这里有个经验把所有绘制代码放在一个函数里函数返回当前帧需要更新的所有 Artist 对象动画循环里才能统一刷新。def draw_person(ax, x0, y0, scale1.0, color#34495e): 在 (x, y) 处画一个火柴人scale 控制整体大小 head plt.Circle((x, y 0.45 * scale), 0.12 * scale, colorcolor, zorder3) # 躯干从脖子到骨盆 body, ax.plot([x, x], [y 0.35 * scale, y - 0.15 * scale], colorcolor, lw2.5 * scale, zorder2) # 左臂向下张开 arm_left, ax.plot([x, x - 0.25 * scale], [y 0.2 * scale, y - 0.05 * scale], colorcolor, lw2 * scale, zorder2) # 右臂向上举指向爱心发射方向 arm_right, ax.plot([x, x 0.35 * scale], [y 0.2 * scale, y 0.45 * scale], colorcolor, lw2 * scale, zorder2) # 双腿自然分开站立 leg_left, ax.plot([x, x - 0.15 * scale], [y - 0.15 * scale, y - 0.45 * scale], colorcolor, lw2 * scale, zorder2) leg_right, ax.plot([x, x 0.15 * scale], [y - 0.15 * scale, y - 0.45 * scale], colorcolor, lw2 * scale, zorder2) ax.add_patch(head) return [head, body, arm_left, arm_right, leg_left, leg_right]zorder参数控制绘制层级值大的画在上面这里把头部放在最上层值为 3四肢都在下层。所有线段用相同颜色#34495e这个颜色是深灰蓝比纯黑柔和在彩色背景上更耐看。如果人物要站在不同位置只需要改传入的x、y要人物大小不同改scale即可所有坐标都是相对量。这段代码里有两处值得注意body等变量名后面的逗号是解包语法因为ax.plot()返回的是一个列表可以画多条线这里只有一条所以取出来单独用返回值把 head 和其他线放在一个列表里是为了动画更新时统一处理。这个函数的输出不是画而是「在画布上新增了 6 个元素并返回它们的引用」后续动画更新就是修改这些引用。2.4 爱心曲线一个方程搞定轮廓和填充爱心图形用经典的心形参数方程这是数学上最简洁、效果也最好的一种表达x 和 y 都是参数 t 的函数t 从 0 到 2π 扫一圈就能闭合。直接用这个方程画散点连线比手动描点靠谱得多。t np.linspace(0, 2 * np.pi, 200) x_heart 16 * np.sin(t) ** 3 y_heart 13 * np.cos(t) - 5 * np.cos(2 * t) - 2 * np.cos(3 * t) - np.cos(4 * t)这个方程组是心形线的经典公式16 * sin³(t)控制左右两瓣的宽度后半部分四个余弦项的组合决定了心形底部的尖角和顶部的凹陷。200 个采样点已经足够让曲线看起来光滑再增加点数对视觉提升不大但会拖慢动画性能。绘制时把x_heart和y_heart整体缩放和平移就能放在画布任意位置def draw_heart(ax, center_x, center_y, scale0.2, color#e74c3c): xs x_heart * scale center_x ys y_heart * scale center_y heart ax.fill(xs, ys, colorcolor, alpha0.9, zorder4) return heartax.fill()返回的是一个 Polygon 对象列表它比plot()画轮廓再单独填色更直接函数内部自动完成封闭。scale 取 0.2 时心形宽度约为 16 * 0.2 * 2 ≈ 6.4 个单位在 -6 到 6 的横轴范围里刚好能完整显示。color 用默认的红色#e74c3calpha 0.9 留一点透明度方便跟后面的动画效果叠加。到这里静态元素已经齐了——一个站在原点的小人以及一颗可以用任意中心点和大小绘制的心形。3. 让爱心「射」出去用 FuncAnimation 构建发射动画核心3.1 FuncAnimation 的工作原理每一帧都是一次函数调用静态图形到位后动画要解决的核心问题是连续帧之间的状态变化。FuncAnimation 的机制非常直接用户提供一个更新函数它接收当前帧号作为参数在函数里修改之前画好的 Artist 对象的属性位置、颜色、大小等然后返回这些被修改的对象matplotlib 每过interval毫秒调用一次这个函数把上一帧的画面替换成新的画面。这个「先画全部再逐帧改属性」的模式比每帧重新绘制整个画布性能好得多因为 Python 对象的属性更新走的是 C 层接口开销更小。发射动作可以用一个简单的物理模型描述爱心从发射点出发沿直线或者弧线飞向目标点。对大多数展示场景直线加缓动就够了——缓动easing可以让爱心在出发时稍慢、中途加速、接近目标再减速视觉上比匀速运动自然很多实现方式却很简单给插值系数加一个变换函数。3.2 基础发射动画的完整代码下面这段代码是可运行的完整版本它实现了「小人站在左侧向右上方发射一颗爱心爱心边飞行边轻微旋转到达目标点后保持悬浮」的完整动画import numpy as np import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation # 画布和坐标系 fig, ax plt.subplots(figsize(8, 6)) ax.set_xlim(-6, 6) ax.set_ylim(-1, 8) ax.set_aspect(equal) ax.axis(off) # 心形参数方程全局变量避免每帧重新计算 t np.linspace(0, 2 * np.pi, 200) x_heart_ref 16 * np.sin(t) ** 3 y_heart_ref 13 * np.cos(t) - 5 * np.cos(2 * t) - 2 * np.cos(3 * t) - np.cos(4 * t) def draw_person(ax, x0, y0, scale1.0, color#34495e): head plt.Circle((x, y 0.45 * scale), 0.12 * scale, colorcolor, zorder3) body, ax.plot([x, x], [y 0.35 * scale, y - 0.15 * scale], colorcolor, lw2.5 * scale, zorder2) arm_left, ax.plot([x, x - 0.25 * scale], [y 0.2 * scale, y - 0.05 * scale], colorcolor, lw2 * scale, zorder2) arm_right, ax.plot([x, x 0.35 * scale], [y 0.2 * scale, y 0.45 * scale], colorcolor, lw2 * scale, zorder2) leg_left, ax.plot([x, x - 0.15 * scale], [y - 0.15 * scale, y - 0.45 * scale], colorcolor, lw2 * scale, zorder2) leg_right, ax.plot([x, x 0.15 * scale], [y - 0.15 * scale, y - 0.45 * scale], colorcolor, lw2 * scale, zorder2) ax.add_patch(head) return [head, body, arm_left, arm_right, leg_left, leg_right] # 发射点小人的右手位置人物在 x0 处 start_x, start_y 0.35, 0.45 # 目标点右上方 target_x, target_y 4.5, 5.0 total_frames 100 # 动画总帧数 # 初始时把爱心放在发射点scale 很小几乎看不见 xs x_heart_ref * 0.1 start_x ys y_heart_ref * 0.1 start_y heart ax.fill(xs, ys, color#e74c3c, alpha0.9, zorder4)[0] # 画小人并记录所有需要更新的 artist person_artists draw_person(ax, x0, y0, scale1.0) def ease_in_out_cubic(p): 三次缓动函数开始慢、中间快、结束慢p 为 0~1 的时间比例 if p 0.5: return 4 * p * p * p else: return 1 - (-2 * p 2) ** 3 / 2 def update(frame): progress frame / total_frames # 0.0 ~ 1.0 线性进度 eased ease_in_out_cubic(progress) # 缓动后的进度 # 插值爱心中心从发射点到目标点 cx start_x (target_x - start_x) * eased cy start_y (target_y - start_y) * eased # 爱心从小到大展开到达后保持原大小 heart_scale 0.1 0.25 * min(eased * 2, 1.0) # 更新爱心的顶点坐标 new_xs x_heart_ref * heart_scale cx new_ys y_heart_ref * heart_scale cy heart.set_xy(np.column_stack([new_xs, new_ys])) # 超出帧数范围时保持爱心在目标位置 if frame total_frames: heart.set_alpha(0.9) return [heart] person_artists anim FuncAnimation(fig, update, framestotal_frames 40, interval33, blitFalse) plt.show() # 保存为 GIF需要 pillow 库 # anim.save(love_launch.gif, writerpillow, fps30)update函数是这段代码的核心progress是 0 到 1 的帧进度eased是缓动后的值两者之间的关系由ease_in_out_cubic决定。爱心每一帧的位置是通过线性插值算出来的——发射点坐标加上目标点与发射点之差乘以进度系数这样不管总帧数怎么改爱心始终从同一个位置出发、在同一点结束。heart_scale做了个小技巧前 50 帧爱心从 0.1 倍大小放大到 0.35 倍看起来像从手里「弹出」之后保持这个大小不再变化避免爱心在飞行中被拉伸变形。heart.set_xy()是修改填充多边形的核心方法传入两列数组即可更新形状。参数方面interval33表示每帧间隔 33 毫秒约等于 30 帧每秒这个数值在人眼看来是流畅的。blitFalse每次更新会重绘整个画布对当前这个简单场景没问题如果以后要画很复杂的背景改成blitTrue可以只更新变化部分代价是必须准确返回所有变化的对象。动画循环次数用的是total_frames 40留出 40 帧让爱心在目标点停留一小会儿再循环。3.3 保存动画GIF 和 MP4 两种导出路径在 Jupyter Notebook 里运行上面代码会直接显示动画但多数场景需要把结果保存成文件。GIF 是传递最方便的形式直接支持网页和聊天工具嵌入MP4 画质更好、文件更小适合长视频剪辑。保存时注意两点GIF 依赖 Pillow 库首次使用前需要安装写writerpillow参数时不要写成writerimagemagick这会让没装 ImageMagick 的机器直接报错。pip install pillowanim.save(love_launch.gif, writerpillow, fps30) anim.save(love_launch.mp4, writerffmpeg, fps30)这里fps30是输出帧率与interval33ms匹配1/0.033 ≈ 30.3fps。如果想调慢播放速度可以同时改interval和fps两个参数比如interval50配fps20动画节奏会舒缓不少适合表现「飘逸的爱心」这种感觉。MP4 保存时如果报错找不到 ffmpeg说明系统里没装这个编码器Windows 用户可以安装 FFmpeg 后把路径加进 PATH 环境变量macOS 可以直接brew install ffmpeg。4. 动画参数的调优与常见故障排查4.1 三个必调参数interval / frames / blit 的边界在哪interval、frames、blit这三个参数决定了动画的流畅度、时长和刷新策略也是最容易改出问题的三个地方。interval是帧间延时毫秒数低于 10 时 matplotlib 的刷新速度跟不上画面会卡但不是因为代码逻辑而是渲染瓶颈高于 100 时能明显看到顿挫感适合做逐帧演示。frames是总帧数它决定了动画的一个循环有多长——注意这里的「帧」不是时间概念而是传给 update 函数的整数序列范围所以frames100配合interval33一个循环就是 3.3 秒。blit对性能的影响在小场景里不明显但对有渐变背景、网格线或者日志输出的复杂画布blitTrue通常能提升 2 到 3 倍帧率。参数推荐值范围影响常见误用interval20 ~ 50 ms播放速度误以为越小越流畅实际 10 会触发渲染瓶颈frames80 ~ 200动画时长与轨迹密度帧数越多轨迹越平滑但 GIF 文件体积线性增长blitFalse → True绘制性能开 blit 后忘记返回所有更新对象导致画面残留4.2 爱心变形坐标轴比例与 set_aspect 的坑最常见的「爱心变椭圆」问题根源几乎都是坐标轴纵横比没锁定。matplotlib 默认会缩放两个轴的显示比例来填满画布所以 16 个单位宽的爱心在横轴上被压缩、在纵轴上被拉伸看起来就是扁的。解决办法是在设置坐标范围后立刻调用ax.set_aspect(equal)让一个单位在屏幕上占据相同像素数。另一个隐蔽的问题是「改数据范围后忘记用 set_aspect」比如为了看清爱心终点把set_xlim(-6, 8)扩展到 14 个单位宽此时即使设了 equal横轴也会因为总宽度增大而显得更「宽」爱心视觉上会缩小。我一般是固定set_xlim的范围用fig.set_size_inches调整画布宽高比来配合目标构图。# 常见坑只改 xlim 不调画布爱心比例悄悄变化 ax.set_xlim(-6, 7) # 左侧 6 右侧 7总共 13 个单位 # 推荐做法动画布尺寸而不是数据的可视范围 fig.set_size_inches(10, 8) ax.set_xlim(-6, 6)这两个方案的区别在于改动xlim是改变坐标系的可视范围等于把整个画面「缩放」改fig.set_size_inches是把画布纸张拉宽坐标系不变图形在屏幕上的真实尺寸相应变大。理解了这个区别构图上就不会出现「爱心到终点变小」的意外。4.3 Jupyter 与脚本运行环境的差异处理同一段动画代码在.py脚本和 Jupyter Notebook 里的表现可能完全不同。脚本里直接plt.show()会弹出一个独立窗口动画在这个窗口里播放Notebook 里如果没有额外的魔法命令动画可能不显示或者只显示一个静态帧。解决办法是按运行环境分流在 Notebook 里动画对象创建后要调用plt.show()且把anim对象赋值给顶层变量——这是在跟 Jupyter 的交互式绘图机制绑定防止动画对象被垃圾回收器提前清除。%matplotlib inline # Notebook 顶部加上这一行 from IPython.display import HTML anim FuncAnimation(fig, update, frames100, interval33) plt.show() # 如果想内嵌到 notebook 输出可以转 HTML5 video # HTML(anim.to_html5_video())命令行脚本里如果运行后 Flash 窗口一闪而过通常是 matplotlib 的交互后端没启动。在脚本最前面加上import matplotlib; matplotlib.use(TkAgg)可以强制指定 Tk 后端想彻底不用界面直接保存 GIF 或 MP4 就行全程只靠anim.save()不依赖显示环境。这类问题排查时先分清是「画面没画出来」还是「画面画了但闪退」前者查后端配置后者查变量作用域——anim必须在模块层有引用。4.4 动画滞涩或掉帧顶点数、alpha 和对象数之间的平衡当画面元素变多比如同时发射 5 颗爱心、背景加了网格动画帧率会急剧下降。这时候优先检查三件事每帧重算顶点有没有必要、透明度层数是否过多、返回的 artist 列表是否太庞大。爱心曲线采样 200 个点已经够平滑再往上加到 1000 个点fill 的顶点数直接变成原来的 5 倍重绘成本线性上升。透明度 alpha 带来的是合成开销每次set_alpha都会触发重新混合连续动画里频繁调用会很贵。我建议 animation 里画心的顶点用 100 个点静态展示时才用 200 个点追求极致平滑。# 动画专用低采样心形顶点数减半视觉几乎无差异 t_anim np.linspace(0, 2 * np.pi, 100)一个实用的性能基准当你在 update 里对每个爱心都要执行set_xy时单帧重绘时间超过 30ms 人就能感知掉帧。代码层面把「三角函数计算」移到全局只算一次update 里只做加减法和乘法能显著降低每帧的 Python 层开销。如果还是卡就把blitTrue打开这是最后一道性能保障。5. 进阶技巧用时间轴数组管理多颗爱心的连发掌握了单颗爱心的发射逻辑把它扩展成「一次动画里发射多颗爱心」反而比想象中简单——只需要把每颗爱心定义成一个数据对象记录它的发射帧、起点、终点和状态然后在 update 里统一遍历。这个模式叫时间轴动画管理是游戏开发里 sprite 管理的简化版。class HeartShot: def __init__(self, start_frame, start_pos, target_pos, duration60): self.start_frame start_frame self.start_pos np.array(start_pos) self.target_pos np.array(target_pos) self.duration duration self.heart None # 在初始化函数里用 ax.fill 创建 # 初始位置放在起点大小为 0不可见 xs x_heart_ref * 0.01 start_pos[0] ys y_heart_ref * 0.01 start_pos[1] self.heart ax.fill(xs, ys, color#e74c3c, alpha0.9, zorder4)[0] def update_state(self, frame): local_frame frame - self.start_frame if local_frame 0: return # 还没发射 if local_frame self.duration: return # 已到达保持不动 progress local_frame / self.duration eased ease_in_out_cubic(progress) cx self.start_pos[0] (self.target_pos[0] - self.start_pos[0]) * eased cy self.start_pos[1] (self.target_pos[1] - self.start_pos[1]) * eased scale 0.1 0.25 * min(eased * 2, 1) self.heart.set_xy(np.column_stack([x_heart_ref * scale cx, y_heart_ref * scale cy])) # 使用三颗爱心分别在第 0、30、60 帧发射 shots [ HeartShot(0, [0.4, 0.5], [4.5, 5.0]), HeartShot(30, [0.4, 0.5], [2.0, 6.0]), HeartShot(60, [0.4, 0.5], [-3.0, 3.5]), ] def update_multi(frame): shots[0].update_state(frame) shots[1].update_state(frame) shots[2].update_state(frame) return [s.heart for s in shots] person_artists这个设计的关键在于start_frame是每颗爱心的「出生时间」update 函数只负责根据「当前帧减去出生帧」计算局部进度。延迟发射不需要额外写逻辑就是设置不同的start_frame不同目标点通过HeartShot构造参数传入。把爱心数据封装成类的好处是新增一颗爱心就是新增一行HeartShot(...)动画主循环完全不用改代码的可读性也大幅提升。说到优化空间ease_in_out_cubic只是缓动函数的一种想尝试更多效果可以换ease_out_bounce爱心落地反弹或ease_in_quad快速弹出实现都是替换这个函数体。爱心发射后的旋转运动也有简单的做法——在set_xy之前把心形坐标先做一个旋转矩阵变换绕自己的中心转 0~15 度能让飞行过程更灵动。你甚至可以给HeartShot加一个trail参数在爱心经过的轨迹上画几个越来越淡的小圆作为残影这些效果都是基于时间轴数组做的延伸主体结构已经不需要再动了。本文还有配套的精品资源点击获取