首页 / 文档中心 / 复刻

3Blue1Brown 复刻(一):圆的面积与多边形逼近

发布于 2026-11-02 · 复刻系列 · 约 5 分钟读完 · 中文正文 + English TL;DR
▶ 渲染效果实录(manim 渲染输出,代码见正文,可直接复现)
English TL;DR

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 的运镜吃透的过程。

复刻心法

  1. 先分镜后代码:把原片按"一次运镜"切分,每段对应一个 play。
  2. 找"驱动参数":3b1b 的动画几乎都有一个核心参数在动(边数、矩阵、斜率),找到它用 ValueTracker 驱动,代码量减半。
  3. 别追求逐帧还原:复刻的目的是学手法,节奏与结构对齐即可。

下一篇复刻:线性变换与矩阵的几何意义。