你在用 Manim 制作一次函数图像的对比动画时,是不是也遇到过这种麻烦:想直观展示不同斜率 k 和截距 b 对直线的影响,但每改一个参数,都得重新手算两端点坐标、重新算与坐标轴的交点,甚至要凭感觉"拉长"线段保证它贯穿画面。
改三组参数,工作量就翻三倍。
今天这篇文章,就是要彻底解决这个体力活。我会带你用 SymPy 把计算交给代码,让 Manim 只负责"画",实现一次函数图像的自动化生成与对比。
1. 痛点场景还原
假设我们想做一个简单的对比动画,在坐标系里同时画出:
- y = 2x + 1
- y = -\\frac{1}{2}x + 3
如果纯用 Manim 手写,我们一般会这样写(只画其中一条的片段):
python
from manim import *
class ManualLinear(Scene):
def construct(self):
ax = Axes(
x_range=[-5, 5],
y_range=[-5, 5],
axis_config={"include_numbers": True}
)
# 手动计算两个点的坐标,以保证线段能覆盖整个画面
# y = 2x + 1,当 x=-5 时 y=-9,当 x=5 时 y=11
line1 = Line(ax.c2p(-5, -9), ax.c2p(5, 11), color=RED)
# 手动计算与 y 轴的交点 (0, 1)
intercept_dot = Dot(ax.c2p(0, 1), color=YELLOW)
self.add(ax, line1, intercept_dot)

这里的问题很明显:端点坐标、截距坐标都是我"算出来写死"的。
如果想把 k 改成 -0.7,b 改成 2.5,上面所有数字都得重新算一遍。
更难受的是,如果想让线段刚好卡在坐标轴的边框上(既不超出也不短),还需要解方程求直线与矩形边框的交点------手动做实在太低效了。
这还只是一条线,如果要一次性展示 k 从 -2 到 2 的多条直线,手动计算根本不可能。
2. SymPy 解决方案:把计算"外包"出去
解决思路非常直接:用 SymPy 负责符号运算,根据给定的参数自动求出我们需要的所有坐标。
核心任务有三个:
- 给定
k、b和坐标系可视范围,自动生成直线的两个端点(正好落在边框上) - 自动求出直线与坐标轴的交点(截距)
- 判断两条直线是否平行(系数比较)
先看纯 SymPy 的运算逻辑,不需要 Manim:
python
import sympy as sp
x, y = sp.symbols('x y')
k, b = sp.symbols('k b')
expr = k * x + b # y = kx + b
# 示例:取 k=2, b=1,x 范围 [-5, 5],y 范围 [-5, 5]
x_min, x_max = -5, 5
y_min, y_max = -5, 5
# 1. 求与坐标轴的交点
x_intercept = sp.solve(expr.subs({k: 2, b: 1}), x) # 令 y=0
# x_intercept = [-1/2] 即 (-0.5, 0)
y_intercept = expr.subs({k: 2, b: 1, x: 0}) # 令 x=0
# y_intercept = 1 即 (0, 1)
# 2. 自动求边框端点:解直线与 x=x_min, x=x_max, y=y_min, y=y_max 的交点,
# 保留落在矩形范围且是"极值方向"的两个点
points_on_border = []
for x_val in (x_min, x_max):
y_val = expr.subs({k: 2, b: 1, x: x_val})
if y_min <= y_val <= y_max:
points_on_border.append((x_val, y_val))
for y_val in (y_min, y_max):
sol_x = sp.solve(expr.subs({k: 2, b: 1}) - y_val, x)
for x_sol in sol_x:
if x_min <= x_sol <= x_max:
points_on_border.append((x_sol, y_val))
# 取两个端点(按 x 排序即可)
points_on_border = sorted(points_on_border, key=lambda p: p[0])
endpoints = [points_on_border[0], points_on_border[-1]]
# 3. 判断平行:比较化简后的系数(注意避免浮点精度问题)
k1, k2 = sp.sympify('2'), sp.sympify('-0.5')
parallel = sp.simplify(k1 - k2) == 0 # 完全相等才平行
上面的计算过程被封装成一个工具函数后,接下来 Manim 只需要拿着这些坐标画图就行了。
3. Manim 联动实战:完整可运行代码
下面给出完整的场景代码,一次运行自动生成 y=kx+b 多条直线的对比图,带截距高亮和平行判断。
python
from manim import *
import sympy as sp
class AutoLinearComparison(Scene):
def construct(self):
# 坐标轴及范围
ax = Axes(
x_range=[-4, 4, 1],
y_range=[-4, 4, 1],
x_length=8,
y_length=6,
axis_config={"include_numbers": True, "font_size": 18},
tips=False,
).add_coordinates()
self.add(ax)
# 需要对比的参数列表:(k, b, 颜色)
params = [
(2, 1, RED),
(-0.5, 3, BLUE),
(1, -2, GREEN),
(-0.5, -1, ORANGE),
]
lines_vg = VGroup()
dots_vg = VGroup()
labels_vg = VGroup()
x_min, x_max = ax.x_range[0], ax.x_range[1] # -6, 6
y_min, y_max = ax.y_range[0], ax.y_range[1] # -4, 4
x, y = sp.symbols("x y")
k_sym, b_sym = sp.symbols("k b")
expr_template = k_sym * x + b_sym # 符号模板
for k_val, b_val, color in params:
# ---- SymPy 计算 ----
expr = expr_template.subs({k_sym: k_val, b_sym: b_val}) # 代入具体参数
# 1. 求直线与坐标轴交点(截距)
x_int = sp.solve(expr, x) # 令 y=0
x_int = float(x_int[0]) if x_int else None
y_int = float(expr.subs(x, 0)) # 令 x=0
# 2. 求直线与矩形边框的合理端点
border_pts = []
for x_val in (x_min, x_max):
y_val = float(expr.subs(x, x_val))
if y_min <= y_val <= y_max:
border_pts.append((x_val, y_val))
for y_val in (y_min, y_max):
sol_x = sp.solve(expr - y_val, x)
for sx in sol_x:
sx_f = float(sx)
if x_min <= sx_f <= x_max:
border_pts.append((sx_f, y_val))
border_pts = sorted(border_pts, key=lambda p: p[0])
# 取首尾作为线段端点
p1, p2 = border_pts[0], border_pts[-1]
# ---- Manim 绘制 ----
line = Line(ax.c2p(*p1), ax.c2p(*p2), color=color, stroke_width=4)
lines_vg.add(line)
# 截距点(如果落在坐标轴范围内)
if x_int is not None and y_min <= 0 <= y_max:
dot_x = Dot(ax.c2p(x_int, 0), color=color, radius=0.08)
dots_vg.add(dot_x)
# 标注 x 截距坐标
label_x = MathTex(
f"({x_int:.1f},0)", font_size=20, color=color
).next_to(dot_x, DOWN)
labels_vg.add(label_x)
if y_int is not None and x_min <= 0 <= x_max:
dot_y = Dot(ax.c2p(0, y_int), color=color, radius=0.08)
dots_vg.add(dot_y)
label_y = MathTex(
f"(0,{y_int:.1f})", font_size=20, color=color
).next_to(dot_y, LEFT)
labels_vg.add(label_y)
# 播放动画
self.play(Create(lines_vg), run_time=3)
self.play(FadeIn(dots_vg, scale=0.5), Write(labels_vg), run_time=2)
self.wait(2)

说明几点关键设计:
x_range[0], x_range[1]直接读取坐标轴的数值范围,后续所有计算都以此为基准,修改范围再也不用手动改端点计算。- 与边框求交时,遍历了四条边界线
x=min, x=max, y=min, y=max,并筛选落在范围内的点,确保线段两端刚好"顶"到边框,不多不少。 - 截距点用了
sp.solve(expr, x)求 x 截距(即 y=0 时 x 的值),用.subs(x,0)求 y 截距。这些值可直接传给ax.c2p完成坐标转换。 - 平行判断在这个例子里没显示,但你可以轻松加入:用
sp.simplify(k1 - k2) == 0比较两条直线的斜率,如果平行就给特殊标注。
4. 效果展示说明
运行上述代码后,你会看到:
- 坐标系先出现,随后四条不同颜色的直线同时生长出来。
- 每条直线的长度恰好贯穿整个画面,没有任何线段伸到坐标轴之外或中途截断,视觉效果干净利落。
- 紧接着,每个颜色对应的截距点(与 x 轴、y 轴的交点)以圆点浮现,旁边自动标注坐标数值,像是"( -0.5 , 0 )"、"( 0 , 3 )" 这样的形式。
- 如果两条直线的 k 值相等(比如再补一条平行线),你还可以添加文字提示"这两条直线平行",彻底不用人工判断。
更棒的是,如果你想换成另外一组 k、b 组合,只需要改动 params 列表,其余一切自动计算、自动适应。比如演示"k 逐渐增大时直线越来越陡",直接写个循环生成 10 条线,瞬间得到教学需要的对比图。
5. 小结
这一期我们解决了一个非常具体的教学动画痛点:手工计算直线端点与截距。通过引入 SymPy,我们实现了:
- 表达式符号化 :
y = kx + b作为模板,替换参数即可得到具体表达式。 - 端点自动生成:解直线与坐标轴矩形的交点,再也不用担心线段太长或太短。
- 截距自动标注 :
solve和subs精确求出与轴的交点,无手动误差。 - 易于扩展:可以轻松加入平行/相交判断、动态改变 k 或 b 的动画等。
当你把繁琐的计算全部外包给 SymPy 后,Manim 就回归了它最擅长的角色:一个纯粹的视觉表达工具。
你的创意不再被重复的算术打断,这才是代码动画应有的样子。