做 Manim 动画演示三角形全等判定定理时,我需要根据给定的边长或角度条件,在坐标系中精确放置三角形的三个顶点。
手动调整点的位置来凑 SSS、SAS 这些条件,反复试错、坐标对不齐,根本没法精确展示"给定条件后三角形唯一确定"这个核心结论。
这篇文章用 SymPy 把几何约束转化为代数方程,自动解出顶点坐标,让动画精准且高效。
1. 痛点场景还原
假设我要做一个 SSS 全等判定的演示:给定三边长度 AB=4, BC=3, AC=5,在坐标系中画出这个三角形,然后改变边长观察三角形是否唯一确定。
如果纯手动操作,我会这样写:
python
from manim import *
import numpy as np
class PainfulSSSDemo(Scene):
def construct(self):
# 手动指定顶点坐标 ------ 怎么知道这三个坐标能满足边长条件?
A = np.array([0, 0, 0]) # 固定 A 在原点
B = np.array([4, 0, 0]) # 固定 B 在 (4,0),这样 AB=4
# C 的位置?需要同时满足 AC=5, BC=3
# 只能手动解方程:x²+y²=25, (x-4)²+y²=9
# 手算得 x=4, y=3,于是 C=(4,3) ------ 但这是直角三角形特例
# 换一组边长又要重新手算,而且每次都要判断镜像解
C = np.array([4, 3, 0])
triangle = Polygon(A, B, C)
self.add(triangle)
核心痛点:
- 给 A、B 定好位置后,C 的坐标必须同时满足 AC 和 BC 的距离约束,手算就是解二元二次方程组,换个边长就要重算一次。
SAS和ASA更麻烦,涉及角度条件,需要把" ∠A=60∘"转化为向量点积方程,手算极易出错。- 方程组通常有两组解(镜像三角形),需要判断哪个是合理的朝向。
- 手动算出来的坐标是近似值,动画中顶点位置不够精准。
这些计算本质上就是几何约束求解 ,完全可以交给 SymPy 自动完成。
2. SymPy 解决方案介绍
SymPy 可以将几何条件转化为代数方程,然后自动求解顶点坐标。
2.1 SSS 全等:已知三边求顶点
已知 A=(0,0), B=(c,0),求 C=(x,y) 满足 AC=b, BC=a。
python
import sympy as sp
x, y = sp.symbols('x y', real=True)
a, b, c = 3, 5, 4 # BC, AC, AB
# 距离约束转化为方程
eq1 = (x - 0)**2 + (y - 0)**2 - b**2 # AC = 5
eq2 = (x - c)**2 + (y - 0)**2 - a**2 # BC = 3
solutions = sp.solve([eq1, eq2], (x, y))
# 输出两组解:[(4, -3), (4, 3)] ------ 对应镜像三角形
对于 SAS 和 ASA,只需把角度条件用向量点积或余弦定理表示,同样构建方程组求解。
2.2 SAS 全等:已知两边和夹角求第三顶点
已知 AB=c, AC=b, ∠A=θ, A 在原点, B 在 (c,0), C 满足到 A 的距离为 b、到 B 的距离用余弦定理求:
python
import sympy as sp
x, y = sp.symbols('x y', real=True)
b, c, theta = 4, 5, sp.rad(60) # AC=4, AB=5, ∠A=60°
# C 到 A 的距离
eq1 = x**2 + y**2 - b**2
# 用余弦定理:BC² = AB² + AC² - 2·AB·AC·cosθ
bc_sq = c**2 + b**2 - 2*c*b*sp.cos(theta)
eq2 = (x - c)**2 + y**2 - bc_sq
solutions = sp.solve([eq1, eq2], (x, y))
2.3 筛选合理解
SymPy 会返回两组解(关于 AB 所在直线对称),通过指定 y 的正负号可以筛选:
python
# 筛选 y >= 0 的解(取上方三角形)
valid_solution = [sol for sol in solutions if sol[1] >= 0][0]
这样就能得到唯一确定的三角形顶点坐标,完美支撑全等判定定理的可视化。
3. Manim 联动实战
下面是一个完整的动画场景,用 ValueTracker 控制边长,动态展示 SSS 全等下三角形的唯一确定性。
python
from manim import *
import sympy as sp
import numpy as np
class SSSCongruenceDemo(Scene):
def construct(self):
# 固定两个顶点
A = np.array([-1, 0, 0])
B = np.array([1, 0, 0])
# 可调边长
a_tracker = ValueTracker(2) # BC
b_tracker = ValueTracker(2) # AC
# 用 always_redraw 动态更新三角形(两个镜像三角形)
triangles = always_redraw(
lambda: self.get_triangles(
A, B, a_tracker.get_value(), b_tracker.get_value()
)
)
self.add(triangles)
# 顶点标签
labels = always_redraw(
lambda: self.get_labels(A, B, a_tracker.get_value(), b_tracker.get_value())
)
self.add(labels)
# 边长标注
side_labels = always_redraw(
lambda: self.get_side_labels(
A, B, a_tracker.get_value(), b_tracker.get_value()
)
)
self.add(side_labels)
# 动画:改变 BC 和 AC 的长度
self.play(a_tracker.animate.set_value(3), run_time=2)
self.play(b_tracker.animate.set_value(1), run_time=2)
self.play(
a_tracker.animate.set_value(2), b_tracker.animate.set_value(2), run_time=2
)
self.wait()
def solve_vertex_C(self, A, B, a, b):
"""用 SymPy 求解顶点 C,返回两个镜像点的 np.array 坐标列表"""
x, y = sp.symbols("x y", real=True)
# AB 的长度
c = np.linalg.norm(B - A)
# 距离约束方程
eq1 = (x - 0) ** 2 + (y - 0) ** 2 - b**2 # 以 A 为原点
eq2 = (x - c) ** 2 + (y - 0) ** 2 - a**2 # 以 B 为原点
solutions = sp.solve([eq1, eq2], (x, y), dict=True)
if not solutions:
return []
# 转换到实际坐标系
AB_vec = B - A
x_axis = AB_vec / c
y_axis = np.array([-x_axis[1], x_axis[0], 0])
C_points = []
for sol in solutions:
sol_x = float(sp.N(sol[x]))
sol_y = float(sp.N(sol[y]))
# 局部坐标转全局坐标
C = A + sol_x * x_axis + sol_y * y_axis
C_points.append(C)
return C_points
def get_triangles(self, A, B, a, b):
C_points = self.solve_vertex_C(A, B, a, b)
if not C_points:
return VGroup() # 无法构成三角形时返回空
triangles = VGroup()
colors = [BLUE, GREEN]
for i, C in enumerate(C_points):
triangle = Polygon(
A, B, C, color=colors[i % 2], fill_opacity=0.3, stroke_width=2
)
triangles.add(triangle)
return triangles
def get_labels(self, A, B, a, b):
C_points = self.solve_vertex_C(A, B, a, b)
if not C_points:
return VGroup()
label_A = MathTex("A", color=WHITE, font_size=28).next_to(A, DL, buff=0.15)
label_B = MathTex("B", color=WHITE, font_size=28).next_to(B, DR, buff=0.15)
labels = VGroup(label_A, label_B)
for i, C in enumerate(C_points):
direction = UP if C[1] >= A[1] else DOWN
label_C = MathTex(f"C_{i+1}", color=WHITE, font_size=28).next_to(
C, direction, buff=0.15
)
labels.add(label_C)
return labels
def get_side_labels(self, A, B, a, b):
C_points = self.solve_vertex_C(A, B, a, b)
if not C_points:
return VGroup()
c = np.linalg.norm(B - A)
labels = VGroup()
# AB 边长标注(共用)
label_AB = MathTex(f"{c:.1f}", font_size=24, color=YELLOW).move_to(
(A + B) / 2 + DOWN * 0.3
)
labels.add(label_AB)
# 每个三角形的 AC 和 BC 边长标注
for i, C in enumerate(C_points):
direction = LEFT if C[0] < (A[0] + B[0]) / 2 else RIGHT
label_AC = MathTex(f"{b:.1f}", font_size=24, color=RED).move_to(
(A + C) / 2 + direction * 0.3
)
label_BC = MathTex(f"{a:.1f}", font_size=24, color=RED).move_to(
(B + C) / 2 + (-direction) * 0.3
)
labels.add(label_AC, label_BC)
return labels

关键点解释:
solve_vertex_C是核心:将 A、B 固定后,以 A 为原点、AB 为 x 轴建立局部坐标系,用SymPy解二元二次方程组求C的局部坐标,再通过向量旋转平移到全局坐标系。- 同时显示 C 点两个解的镜像,上下两个全等三角形在视觉上稳定一致。
always_redraw保证边长改变时三角形实时更新,顶点坐标由SymPy自动重新计算,完全不用手动干预。- 当给定三边长度不满足三角形不等式时,
solve无实解,函数返回None,动画自动隐藏三角形,自然展示了"不是任意三边都能构成三角形"。
4. 效果展示说明
运行这个场景,你会看到:
- 坐标系中, A 和 B 两个顶点固定不变,边长标注清晰显示当前的 AB、 BC、 AC 长度。
- 改变 BC 的长度 : C 点沿一条弧线移动,三角形的形状随之变化,但始终满足给定的三边长度约束。你能直观看到"改变一边,三角形形状唯一确定"。
- 改变 AC 的长度 :类似地, C 点在另一条弧线上移动,三角形的三个顶点自动重新定位。
- 同时改变两边:三角形平滑过渡到新的形状,整个过程顶点坐标精确、无任何手动调整的痕迹。
- 如果输入的边长无法构成三角形(如 a+b⩽c),三角形消失,自然展示了三角形不等式这个隐含条件。
以此为基础,只需修改方程组,就可以扩展为 SAS、ASA、AAS 等其他全等判定的可视化演示。
5. 小结
SymPy 在 Manim 几何动画中的作用可以概括为:将几何约束转化为代数方程,让计算机解出精确的顶点坐标。
你不再需要手动凑坐标、手算方程组、处理镜像解,只需要描述"已知条件是什么",SymPy 就会返回"点应该在哪里"。
这个思路不仅适用于全等三角形,也适用于任何需要精确几何构造的场景:尺规作图、动点轨迹、最值问题等等。
把数学计算交给 SymPy,把视觉表达留给 Manim,两者配合才能做出真正精准而优雅的数学动画。