3Blue1Brown 复刻(一):圆的面积与多边形逼近
Recreating the first minutes of 3Blue1Brown's calculus essence: why does a circle's area equal pi r squared? The visual argument is polygon approximation — inscribe a regular n-gon, its area is 0.5·n·sin(2π/n)·r², and as n grows the polygon becomes indistinguishable from the circle. In Manim this is a ValueTracker driving an always_redraw RegularPolygon while a formula DecimalNumber updates live; one animation of the tracker produces the whole sequence n = 4, 8, 16, 32, ... with nothing else to keyframe. We then extend to the rearrangement argument — slicing the disk into thin sectors and alternating them into an approximate rectangle of width πr and height r — using sector wedges and arrange(). Full runnable code for both halves, plus tips on Integer updaters, polygon stroke joins at low n, and pacing the count-up so audiences feel the limit rather than just see it.
要复刻什么
3b1b《微积分的本质》第一期开场:圆的面积为什么是 πr²?答案用动画讲最清楚——内接正多边形随边数增加逼近圆,把"极限"变成看得见的过程。本文用约 40 行 Manim 代码完整复刻。
效果拆解
- 一个固定的圆(半径 r)
- 内接正 n 边形,n 从 4 一路翻倍到 50+
- 面积公式 S(n) = ½·n·sin(2π/n)·r² 实时刷新
- 多边形逐渐"填满"圆——极限的可视化
完整代码
from manim import *
class CircleArea(Scene):
def construct(self):
R = 2.2
circle = Circle(radius=R, color=BLUE, stroke_width=4)
circle.to_edge(LEFT, buff=1.2)
title = Text("圆的面积从哪来?", font="Microsoft YaHei",
font_size=34).to_edge(UP)
# 边数由 ValueTracker 驱动
n = ValueTracker(4)
polygon = always_redraw(lambda: RegularPolygon(
n=int(max(3, round(n.get_value()))),
radius=R, color=YELLOW, stroke_width=3,
).move_to(circle.get_center()))
area_val = always_redraw(lambda: DecimalNumber(
0.5 * int(max(3, round(n.get_value())))
* np.sin(2 * np.pi / max(3, round(n.get_value()))) * R**2,
num_decimal_places=4, font_size=44, color=YELLOW,
).next_to(circle, RIGHT, buff=1.6).shift(UP * 1.2))
area_lab = always_redraw(lambda: Text(
"内接多边形面积", font="Microsoft YaHei", font_size=26,
).next_to(area_val, UP, buff=0.3))
n_lab = always_redraw(lambda: MathTex(
"n = %d" % max(3, round(n.get_value())), font_size=44,
).next_to(area_val, DOWN, buff=0.8))
self.play(Create(circle), Write(title))
self.add(polygon, area_val, area_lab, n_lab)
# 边数翻倍逼近: 观众多边形与圆重合的过程
for target in (8, 16, 32, 64):
self.play(n.animate.set_value(target), run_time=2,
rate_func=smooth)
self.wait(0.3)
conclusion = MathTex(r"S = \pi r^2", font_size=64, color=BLUE)
conclusion.next_to(area_val, DOWN, buff=1.6)
self.play(Write(conclusion))
self.wait()
渲染:manim -pqm circle_area.py CircleArea
关键手法讲解
1. ValueTracker 驱动整数参数
RegularPolygon(n=...) 的 n 是整数,而动画插值是连续的。解决:总是 int(round(...)) 再喂给构造器,并 max(3, ...) 防止插值过程中掉到 3 以下报错。
2. always_redraw 三件套同步
多边形、面积数字、n 标签都声明为 n 的函数,一次 play(n.animate.set_value(64)),三个对象同步更新——这就是Updater 章讲的响应式威力。
3. 节奏:翻倍而不是匀增
4→8→16→32→64 每次 play 一段,比 4→65 匀速扫过去更有"逼近感":每段观众都看到多边形明显更贴合圆,最后一段几乎看不出变化——"看不出变化"本身就是极限的直观定义,这正 是原片的节奏设计。
进阶:切片重组成长方形
原片后半段把圆切成细扇形、上下交错排成近似的矩形(宽 πr、高 r)。复刻思路:
sectors = VGroup(*[Sector(
outer_radius=R, angle=TAU / 24,
start_angle=i * TAU / 24, color=interpolate_color(BLUE_D, BLUE_E, i % 2),
fill_opacity=1, stroke_width=0.5,
) for i in range(24)])
self.play(FadeIn(sectors))
# 交错翻转再排列成"矩形"
flipped = VGroup(*[s.copy().rotate(PI) if i % 2 else s.copy()
for i, s in enumerate(sectors)])
flipped.arrange(RIGHT, buff=0.02).move_to(DOWN * 2)
self.play(TransformFromCopy(sectors, flipped))
细节数值(扇形数、buff、翻转方式)值得自己微调,调的过程就是把 3b1b 的运镜吃透的过程。
复刻心法
- 先分镜后代码:把原片按"一次运镜"切分,每段对应一个 play。
- 找"驱动参数":3b1b 的动画几乎都有一个核心参数在动(边数、矩阵、斜率),找到它用 ValueTracker 驱动,代码量减半。
- 别追求逐帧还原:复刻的目的是学手法,节奏与结构对齐即可。
下一篇复刻:线性变换与矩阵的几何意义。