重排证明勾股定理¶
移动四个直角三角形揭示为何 a² + b² = c²。三角形及其外接正方形始终保持面积不变。
examples/pythagoras.py
"""a² + b² = c²,通过移动四个三角形来证明。
将一个直角三角形的四份副本(两直角边为 a 和 b,斜边为 c)放入一个边长为 a + b 的正方形
中,每份各占一角且相对上一份旋转四分之一圈:它们留下的空间是斜边上的一个倾斜正方形,
面积为 c²。现在滑动其中三个,不旋转,直到四份两两配成两个矩形:剩余空间是一个边长为 a
的正方形和一个边长为 b 的正方形。大正方形和四个三角形始终未变,因此它们留下的空间也未
变:a² + b² = c²。
"""
import numpy as np
import manimgx as m
A, B = 2.4, 3.6 # 两直角边,以场景单位计
S = A + B # 大正方形的边长
CORNER = np.array([-5.75, -3.0, 0.0]) # 大正方形的左下角
CENTER = CORNER + np.array([S / 2, S / 2, 0.0])
A_COLOR, B_COLOR, C_COLOR = m.TEAL, m.GOLD, m.RED
TRIANGLE_FILL = m.BLUE_E
EDGE = 6 # 三角形的边线宽度,以单位百分之一计
OPACITY = 0.65 # 剩余空间的填充
WALL = 9.2
def at(x: float, y: float) -> np.ndarray:
"""大正方形中从其左下角起量的点 (x, y)。"""
return CORNER + np.array([x, y, 0.0])
def triangle(right: np.ndarray, a_end: np.ndarray, b_end: np.ndarray) -> m.VGroup:
"""一个直角三角形,直角位于 `right`,两直角边 a 和 b 分别终止于
`a_end` 和 `b_end`:包含其主体、边 a、b 和 c(各自着色)以及
直角标记。"""
body = m.Polygon(right, a_end, b_end, stroke_width=0)
body.set_fill(TRIANGLE_FILL, opacity=1.0)
along_a, along_b = m.normalize(a_end - right), m.normalize(b_end - right)
mark = m.VMobject(stroke_color=m.GREY_A, stroke_width=3)
size = 0.3
mark.set_points_as_corners(
[
right + size * along_a,
right + size * (along_a + along_b),
right + size * along_b,
]
)
return m.VGroup(
body,
m.Line(right, a_end, color=A_COLOR, stroke_width=EDGE),
m.Line(right, b_end, color=B_COLOR, stroke_width=EDGE),
m.Line(a_end, b_end, color=C_COLOR, stroke_width=EDGE),
mark,
)
def body(triangle: m.VGroup) -> m.VMobject:
"""一个三角形的主体:它所覆盖的区域。"""
shape = triangle[0]
assert isinstance(shape, m.VMobject)
return shape
def space_left(outline: m.VMobject, triangles: list[m.VGroup]) -> m.VMobject:
"""大正方形中四个三角形当前未覆盖的部分。"""
covered = m.Union(*(body(t) for t in triangles))
left = m.Difference(outline, covered, stroke_width=0)
return left.set_fill(C_COLOR, opacity=OPACITY)
def label(
tex: str, color: m.ManimColor, x: float, y: float, size: int = 60
) -> m.MathTex:
return m.MathTex(tex, font_size=size, color=color).move_to(at(x, y))
class Pythagoras(m.Scene):
def construct(self) -> None:
outline = m.Polygon(at(0, 0), at(S, 0), at(S, S), at(0, S))
outline.set_stroke(m.GREY_B, width=4)
# 一个三角形在右下角,然后三份副本,每份相对上一份绕正方形中心
# 旋转四分之一圈
first = triangle(at(S, 0), at(S, A), at(A, 0))
a_label = label("a", A_COLOR, S + 0.4, A / 2)
b_label = label("b", B_COLOR, A + B / 2, -0.45)
hypotenuse_mid = (at(S, A) + at(A, 0)) / 2
c_label = m.MathTex("c", font_size=60, color=C_COLOR)
c_label.move_to(hypotenuse_mid + 0.42 * m.normalize(CENTER - hypotenuse_mid))
self.play(m.Create(outline), run_time=1.0)
self.play(
m.FadeIn(first[0]),
*(m.Create(part) for part in first[1:]),
run_time=1.2,
)
self.play(m.Write(a_label), m.Write(b_label), m.Write(c_label), run_time=0.8)
triangles = [first]
for _ in range(3):
turned = triangles[-1].copy()
self.play(m.Rotate(turned, m.PI / 2, about_point=CENTER), run_time=0.8)
triangles.append(turned)
_, upper_right, upper_left, lower_left = triangles
# 两条标注边的另一半:每条边长为 a + b
self.play(
m.FadeIn(label("a", A_COLOR, A / 2, -0.45)),
m.FadeIn(label("b", B_COLOR, S + 0.4, A + B / 2)),
run_time=0.6,
)
# 剩余空间是斜边上的正方形
tilted = m.Polygon(at(A, 0), at(S, A), at(B, S), at(0, B), stroke_width=0)
tilted.set_fill(C_COLOR, opacity=OPACITY)
c_squared = m.MathTex("c^2", font_size=96, color=C_COLOR).move_to(CENTER)
self.add(tilted, *triangles) # 位于三角形边线之下
self.play(
m.FadeIn(tilted), m.FadeOut(c_label), m.Write(c_squared), run_time=1.2
)
equation = m.MathTex("{{a^2}} + {{b^2}} = {{c^2}}", font_size=96)
equation.move_to(np.array([4.0, 0.0, 0.0]))
eq_a, plus, eq_b, equals, eq_c = equation
eq_a.set_color(A_COLOR)
eq_b.set_color(B_COLOR)
eq_c.set_color(C_COLOR)
self.play(m.TransformFromCopy(c_squared[0], eq_c), run_time=1.5)
self.wait(0.5)
# 三次滑动,均不旋转:剩余空间随每个三角形移动而环绕流动
left = m.always_redraw(lambda: space_left(outline, triangles))
self.remove(tilted)
self.add(left, *triangles)
slides = [
(upper_left, at(A, -B) - at(0, 0)), # 横跨倾斜正方形,位移为 c
(upper_right, at(-B, 0) - at(0, 0)), # 沿顶部向左,位移为 b
(lower_left, at(0, A) - at(0, 0)), # 沿侧边向上,位移为 a
]
self.play(
upper_left.animate(run_time=1.6).shift(slides[0][1]),
m.FadeOut(c_squared, run_time=0.5),
)
for moving, by in slides[1:]:
self.play(moving.animate.shift(by), run_time=1.6)
# 剩余空间现在是两个正方形,分别在 a 和 b 上
left.clear_updaters()
a_square = m.Polygon(at(0, 0), at(A, 0), at(A, A), at(0, A), stroke_width=0)
b_square = m.Polygon(at(A, A), at(S, A), at(S, S), at(A, S), stroke_width=0)
for square in (a_square, b_square):
square.set_fill(C_COLOR, opacity=OPACITY)
self.remove(left)
self.add(a_square, b_square, *triangles)
a_squared = m.MathTex("a^2", font_size=80, color=A_COLOR).move_to(a_square)
b_squared = m.MathTex("b^2", font_size=96, color=B_COLOR).move_to(b_square)
self.play(
a_square.animate.set_fill(A_COLOR, opacity=OPACITY),
b_square.animate.set_fill(B_COLOR, opacity=OPACITY),
m.Write(a_squared),
m.Write(b_squared),
run_time=1.2,
)
# 由面积标签得出等式
self.play(m.TransformFromCopy(a_squared[0], eq_a), m.FadeIn(plus), run_time=1.3)
self.play(
m.TransformFromCopy(b_squared[0], eq_b), m.FadeIn(equals), run_time=1.3
)
self.wait(2.0)
if __name__ == "__main__":
Pythagoras().render("pythagoras.mp4")