做追及问题动画时,需要根据题意列方程求出相遇时间,再手动计算两个物体在每个时刻的坐标。
题意中速度、初始距离、出发时间差这些参数一改变,就得重新手算一遍,整个过程繁琐且易错。
本文用 SymPy 把列方程和求解都自动化,直接得到相遇时间和运动轨迹,动画代码只需拿到结果去画图。
1. 痛点场景还原
假设做一个最经典的追及动画:甲从原点出发,速度 v_1=2 ;乙从 x=10 处同向出发,速度 v_2=5 ,问多久追上。
如果用纯手工方式写 Manim:
python
class PainfulCatchUp(Scene):
def construct(self):
# 手动列方程并求解
# 设 t 为乙出发后的时间,甲的位置:2*(t+?),乙的位置:10+5*t
# 如果同时出发:2t = 10+5t → t = -10/3 负数无意义
# 改甲先出发2秒:2(t+2) = 10+5t → 2t+4=10+5t → -3t=6 → t=-2 还是负
# 必须反复调整题设,手算满足实际情况的初始条件
# 这里干脆让乙追甲,甲在乙前面:
# 甲在x=10以v=2向前,乙在x=0以v=5同时出发 → 5t = 10+2t → 3t=10 → t=10/3
v1, v2 = 2, 5
t_meet = 10/3 # 手动解出的结果
meet_x = 5 * t_meet # 再手动算相遇位置
# 甲和乙的轨迹只能硬编码
def pos1(t):
return 10 + v1 * t
def pos2(t):
return v2 * t
# 然后创建动画......
痛点很明显:
- 每次改变速度或初始距离,都要重新手写方程、求解、算相遇坐标。
- 题目条件稍微变化(比如"甲先走1分钟"、"乙在中途休息"),手算的过程就得全部推翻重来。
- 容易在单位换算、正方向等细节上出错,动画一旦跑起来发现不对,排查起来也费劲。
这些计算本质上就是根据文字描述建立代数方程并求解,正是 SymPy 最擅长的事。
2. SymPy 解决方案介绍
SymPy 可以让我们用符号把追及问题"翻译"成方程,然后自动求解。
python
import sympy as sp
# 符号定义:t 为乙出发后经过的时间
t = sp.symbols('t', positive=True)
v1, v2 = 2, 5 # 速度
s0 = 10 # 初始距离(甲在乙前面10米)
# 甲的位置:先出发0秒(即同时出发),位置 = s0 + v1*t
pos1 = s0 + v1 * t
# 乙的位置:从0开始,位置 = v2*t
pos2 = v2 * t
# 相遇条件:位置相等
eq = sp.Eq(pos1, pos2)
solution = sp.solve(eq, t)
# 输出: [10/3]
如果甲先出发 2 秒,方程只需改一下:
python
t_delay = 2 # 甲早出发2秒
pos1 = s0 + v1 * (t + t_delay) # 甲多走2秒
eq = sp.Eq(pos1, pos2)
solution = sp.solve(eq, t)
# 输出: [14/3]
无论怎么变化,我们只需要修改符号表达式的构建逻辑,求解交给 solve,相遇坐标直接代入即可。
接下来把这个思想嵌入 Manim,动画就能自适应任意追及条件。
3. Manim 联动实战
下面是一个完整的动画场景:给定甲、乙的初始位置、速度和出发延迟,自动计算相遇点,并动态展示追及过程。
python
from manim import *
import sympy as sp
class CatchUpLab(Scene):
def construct(self):
# ========== 题目参数(任意修改这里即可) ==========
v1 = 1.5 # 甲的速度
v2 = 2.5 # 乙的速度
init_gap = 8 # 初始距离(甲在乙前面)
delay = 1 # 甲早出发的时间
# ========== SymPy 自动求解 ==========
t = sp.symbols("t", positive=True)
pos1_expr = init_gap + v1 * (t + delay) # 甲的位置
pos2_expr = v2 * t # 乙的位置
eq = sp.Eq(pos1_expr, pos2_expr)
t_meet = sp.solve(eq, t)[0] # 精确解
meet_x = float(pos2_expr.subs(t, t_meet)) # 相遇位置
# 为了动画流畅,预先计算两个运动函数(可以直接用lambda)
def pos1_func(time):
return init_gap + v1 * (time + delay)
def pos2_func(time):
return v2 * time
# ========== 场景搭建 ==========
axes = NumberLine(
x_range=[0, 30, 5],
length=8,
include_numbers=True,
label_direction=DOWN,
)
self.play(Create(axes))
# 甲和乙的点
dot1 = Dot(color=RED, radius=0.2)
dot2 = Dot(color=BLUE, radius=0.2)
# 初始放置
dot1.move_to(axes.number_to_point(pos1_func(0)))
dot2.move_to(axes.number_to_point(pos2_func(0)))
self.add(dot1, dot2)
# 标签
label1 = Text("甲", color=RED).next_to(dot1, UP * 2)
label2 = Text("乙", color=BLUE).next_to(dot2, UP * 2)
self.add(label1, label2)
# 轨迹虚线(预留)
trace1 = TracedPath(dot1.get_center, stroke_color=RED, stroke_width=2)
trace2 = TracedPath(dot2.get_center, stroke_color=BLUE, stroke_width=2)
self.add(trace1, trace2)
# 相遇点标记(先隐藏,等追到时再显示)
meet_dot = Dot(point=axes.number_to_point(meet_x), color=YELLOW)
meet_label = Text(f"相遇点: {meet_x:.2f}", font_size=24, color=YELLOW)
meet_label.next_to(meet_dot, UP * 1.5)
# 动态更新的时间显示
time_text = MathTex("t=0.0").shift(UL * 2)
self.add(time_text)
# 追击动画
total_time = float(t_meet) + 2 # 多跑2秒
def update_dots(mob, alpha):
# alpha 从0到1,对应时间从0到total_time
t_now = alpha * total_time
dot1.move_to(axes.number_to_point(pos1_func(t_now)))
dot2.move_to(axes.number_to_point(pos2_func(t_now)))
# 更新标签位置
label1.next_to(dot1, UP * 2)
label2.next_to(dot2, UP * 2)
# 更新时间显示
time_text.become(MathTex(f"t={t_now:.1f}").shift(UL * 2))
# 判断是否到达相遇点
if t_now >= float(t_meet):
self.add(meet_dot, meet_label) # 显示相遇标记
self.play(
UpdateFromAlphaFunc(
VGroup(dot1, dot2, label1, label2, time_text),
update_dots,
run_time=total_time,
rate_func=linear,
)
)
self.wait(1)

关键点解释:
- 参数集中在开头,修改
v1, v2, init_gap, delay就能改变整个题目,SymPy会自动重新求解相遇时间和位置,动画完全自适应。 - 用
UpdateFromAlphaFunc驱动动画,每一帧根据当前时间计算两个点的坐标,而坐标函数是直接用 SymPy 解出的表达式生成的,精确无误差。 - 相遇点预先用
sp.solve得到精确值,当动画时间超过相遇时刻时,黄色标记出现,直观展示"甲被乙追上"的瞬间。 - 没有使用
always_redraw而采用UpdateFromAlphaFunc,是为了更好地控制动画进度和时间显示,同时避免复杂的依赖更新。
4. 效果展示说明
运行这个场景,你会看到:
- 一条水平数轴,红点(甲)在蓝点(乙)的前方。
- 动画开始后,两点同时向右移动,蓝点速度更快,逐渐逼近红点。
- 在精确的相遇时刻,一个黄色圆点出现在相遇位置,并标注坐标,同时甲和乙重合。
- 即使改变参数------比如甲的速度从 1.5 改成 1.2、初始距离改成 12、甲提前出发 3 秒,只需修改脚本顶部的几个数字,无需任何手动计算,动画仍然能够准确呈现追及过程,并自动在正确的位置标记相遇点。
- 如果参数设置使得无法追上(如乙的速度小于甲),
sp.solve返回空或负解,我们可以加入逻辑判断提示无法相遇,动画就不会标记错误时刻。
5. 小结
追及问题的动画化,真正耗时的往往不是画图,而是根据不断变化的题设反复列方程、解方程、算坐标。
SymPy 的价值在于把"根据题意列方程"和"求解"这两步都程序化了,你只需要用符号描述位置关系,剩下的事交给计算机。
这种思路同样适用于其他行程问题:相遇、环形跑道、顺流逆流......只要你能把物理情景转化为代数方程,
SymPy 就能帮你解出关键节点,而 Manim 负责把这些节点变成流畅的视觉呈现。
两个工具分工明确,做出来的动画既精准又灵活。