展开圆周¶
细环展开为长条,长条堆叠成三角形。其底为 2πr, 高为 r,故面积为 πr²。
examples/circle_area.py
"""为何圆的面积是 πr²:把它展开。
将半径为 r 的圆盘切成薄环。每个环从顶部剪开并展开后,是一条细长条,长度等于其周长(半径为 ρ 的
环为 2πρ),厚度等于环的厚度。将这些长条堆叠起来,最长的在底部:它们的长度从 2πr 均匀缩减到 0,
因此构成一个底为 2πr、高为 r 的直角三角形。其面积为 ½ · 2πr · r = πr²,圆盘的面积亦然:
环越薄,堆叠就越接近该三角形。
"""
import numpy as np
import manimgx as m
WALL = 3.4 # 环展开并堆叠
N = 24 # 环数
R = 1.9 # 圆盘的半径,以场景单位计
DR = R / N # 每个环的厚度
CENTER = np.array([-3.4, 1.75, 0]) # 圆盘的
CORNER = np.array([-5.55, -2.95, 0]) # 三角形的直角,左下角
RINGS = m.color_gradient([m.BLUE, m.TEAL, m.GREEN], N) # 由内向外
RADIUS = m.YELLOW # r,半径及三角形的高
ROUND = m.GREEN # 2πr,最外层的环及三角形的底
SAMPLES = 121
def ring_points(rho: float, unrolled: float, bottom: np.ndarray) -> np.ndarray:
"""半径为 `rho` 的环,从 0(一个圆)展开到 1(一条直长条):一段半径为 rho/(1 − unrolled)、
角度为 2π(1 − unrolled) 的弧,因而长度与圆相同,其最低点保持在 `bottom` 处,且该处切线水平。"""
s = np.linspace(-np.pi * rho, np.pi * rho, SAMPLES) # 从底部起的弧长
k = (1 - unrolled) / rho # 曲率
# sin(ks)/k 和 (1 − cos(ks))/k,写成在 k → 0 时仍精确的形式
x = s * np.sinc(k * s / np.pi)
y = 0.5 * k * s**2 * np.sinc(k * s / (2 * np.pi)) ** 2
return bottom + np.stack([x, y, np.zeros_like(s)], axis=1)
class CircleArea(m.Scene):
def construct(self) -> None:
radii = [(k + 0.5) * DR for k in range(N)]
def ring(
k: int, unrolled: float = 0.0, bottom: np.ndarray | None = None
) -> m.VMobject:
rho = radii[k]
where = CENTER - rho * m.UP if bottom is None else bottom
mob = m.VMobject(stroke_color=RINGS[k], stroke_width=100 * DR * 0.88)
mob.set_points_as_corners(ring_points(rho, unrolled, where))
return mob
disc = m.VGroup(*[ring(k) for k in range(N)])
radius = m.Line(CENTER, CENTER + R * m.RIGHT, color=RADIUS, stroke_width=6)
r_label = m.MathTex("r", color=RADIUS, font_size=56)
r_label.next_to(radius, m.RIGHT, buff=0.18) # 在其端点处,避开环
radius.set_z_index(1) # 在环之上,也在环的副本离开时在其之上
r_label.set_z_index(1)
# 0–3 秒:圆盘,扫过
self.play(*[m.Create(r) for r in disc], run_time=1.6)
self.play(
m.Create(radius), m.FadeIn(r_label, shift=0.2 * m.RIGHT), run_time=0.9
)
# 3–9 秒:每个环从顶部剪开,在落入其在
# 堆叠中的位置时展开:最长的在底部,每条长条从三角形的左边缘开始
progress = [m.ValueTracker(0.0) for _ in range(N)]
def strip(k: int) -> m.VMobject:
u = progress[k].get_value()
rho = radii[k]
start = CENTER - rho * m.UP
end = CORNER + np.pi * rho * m.RIGHT + (R - rho) * m.UP
return ring(k, min(1.0, u / 0.75), start + u * (end - start))
strips = m.VGroup(*[m.always_redraw(lambda k=k: strip(k)) for k in range(N)])
self.add(strips)
self.play(
m.LaggedStart(
*[progress[k].animate.set_value(1.0) for k in reversed(range(N))],
lag_ratio=0.07,
),
run_time=6.5,
)
# 9–11 秒:堆叠是一个直角三角形,宽 2πr、高 r
top = CORNER + R * m.UP
right = CORNER + 2 * np.pi * R * m.RIGHT
outline = m.Polygon(CORNER, right, top, color=m.WHITE, stroke_width=3)
base = m.Line(CORNER, right)
height = m.Line(CORNER, top)
base_brace = m.Brace(base, m.DOWN, buff=0.12, color=ROUND)
base_label = m.MathTex(r"2\pi r", color=ROUND, font_size=52)
base_label.next_to(base_brace, m.DOWN, buff=0.1)
height_brace = m.Brace(height, m.LEFT, buff=0.12, color=RADIUS)
height_label = m.MathTex("r", color=RADIUS, font_size=52)
height_label.next_to(height_brace, m.LEFT, buff=0.12)
self.play(m.Create(outline), run_time=1)
self.play(
m.GrowFromCenter(base_brace),
m.FadeIn(base_label, shift=0.2 * m.DOWN),
m.GrowFromCenter(height_brace),
m.FadeIn(height_label, shift=0.2 * m.LEFT),
run_time=1.2,
)
# 11–18 秒:面积,底乘以高的一半
area = m.MathTex(
r"{{\text{Area} =}} {{\frac{1}{2} \cdot 2}} {{\pi}} {{r}} \cdot {{r}}",
font_size=60,
)
area[1][-1].set_color(ROUND) # 2πr 中的 2(其字形:1, 2, 横线, 点, 2)
m.VGroup(area[2], area[3]).set_color(ROUND)
area[5].set_color(RADIUS)
area.move_to(np.array([2.9, CENTER[1], 0]))
cancelled = m.MathTex(
r"{{\text{Area} =}} {{\pi}} {{r}} \cdot {{r}}", font_size=60
)
m.VGroup(cancelled[1], cancelled[2]).set_color(ROUND)
cancelled[4].set_color(RADIUS)
cancelled.move_to(area, aligned_edge=m.LEFT)
result = m.MathTex(r"{{\text{Area} =}} {{\pi}} {{r}}^{ {{2}} }", font_size=72)
m.VGroup(result[1], result[2]).set_color(ROUND)
result.get_parts_by_tex("2", substring=False).set_color(RADIUS)
result.move_to(area)
self.play(m.Write(area), run_time=1.5)
self.wait(0.5)
# ½ 和 2 相消
self.play(
m.FadeOut(area[1], shift=0.3 * m.DOWN),
*[
m.ReplacementTransform(a, b)
for a, b in zip([area[0], *area[2:]], cancelled, strict=True)
],
run_time=1.2,
)
self.remove(*cancelled)
self.add(cancelled)
# r · r = r²
self.play(m.TransformMatchingTex(cancelled, result), run_time=1.5)
self.wait(2)
if __name__ == "__main__":
CircleArea().render("circle_area.mp4")