chamber-video

物理动画参考

Agent 生成物理类 Manim 代码时的模板库。所有代码片段可直接运行。

电场

点电荷电场线

plus = Dot([-2, 0, 0], radius=0.25, color=RED)
plus_label = Text("+", font_size=32, color=WHITE).move_to(plus)
minus = Dot([2, 0, 0], radius=0.25, color=BLUE)
minus_label = Text("-", font_size=32, color=WHITE).move_to(minus)

lines = VGroup()
for angle in np.linspace(0, 2 * PI, 8, endpoint=False):
    start = [-2, 0, 0] + 0.35 * np.array([np.cos(angle), np.sin(angle), 0])
    pts = [start]
    pos = np.array(start, dtype=float)
    for _ in range(80):
        r1 = pos - np.array([-2.0, 0, 0])
        r2 = pos - np.array([2.0, 0, 0])
        d1 = np.linalg.norm(r1)
        d2 = np.linalg.norm(r2)
        if d1 < 0.3 or d2 < 0.3:
            break
        E = r1 / d1**3 - r2 / d2**3
        E = E / np.linalg.norm(E) * 0.15
        pos = pos + E
        pts.append(pos.copy())
    if len(pts) > 2:
        line = VMobject(stroke_width=1.5, stroke_color=YELLOW, stroke_opacity=0.6)
        line.set_points_smoothly([np.array(p) for p in pts])
        lines.add(line)

self.play(FadeIn(plus), FadeIn(plus_label), FadeIn(minus), FadeIn(minus_label))
self.play(LaggedStart(*[Create(l) for l in lines], lag_ratio=0.05), run_time=2)

磁场

条形磁铁磁场线

bar_n = Rectangle(width=0.6, height=2, color=RED, fill_opacity=0.6).shift(UP * 0.5)
bar_s = Rectangle(width=0.6, height=2, color=BLUE, fill_opacity=0.6).shift(DOWN * 0.5)
n_label = Text("N", font_size=24, color=WHITE).move_to(bar_n)
s_label = Text("S", font_size=24, color=WHITE).move_to(bar_s)
magnet = VGroup(bar_s, bar_n, n_label, s_label)

field_lines = VGroup()
for i in range(6):
    offset = (i - 2.5) * 0.4
    arc = Arc(start_angle=PI * 0.1, angle=PI * 0.8, radius=1.5 + abs(offset) * 0.3,
              color=YELLOW, stroke_width=1.5, stroke_opacity=0.5)
    arc.shift(UP * 0.5 + RIGHT * offset * 0.3)
    field_lines.add(arc)
    arc2 = Arc(start_angle=PI * 1.1, angle=PI * 0.8, radius=1.5 + abs(offset) * 0.3,
               color=YELLOW, stroke_width=1.5, stroke_opacity=0.5)
    arc2.shift(DOWN * 0.5 + RIGHT * offset * 0.3)
    field_lines.add(arc2)

self.play(FadeIn(magnet))
self.play(LaggedStart(*[Create(l) for l in field_lines], lag_ratio=0.08), run_time=2)

电磁波

3D 电磁波传播

class EMWaveDemo(ThreeDScene):
    def construct(self):
        axes = ThreeDAxes(x_range=[0, 8, 1], y_range=[-2, 2, 1], z_range=[-2, 2, 1],
                          x_length=8, y_length=4, z_length=4)
        t = ValueTracker(0)

        e_curve = always_redraw(lambda: axes.plot(
            lambda x: np.sin(x - t.get_value()),
            x_range=[0, 7], color=RED, stroke_width=3,
        ))
        b_curve = always_redraw(lambda: ParametricFunction(
            lambda x: axes.c2p(x, 0, np.sin(x - t.get_value())),
            t_range=[0, 7], color=BLUE, stroke_width=3,
        ))

        e_label = Text("E", font_size=24, color=RED).move_to([3, 2, 0])
        b_label = Text("B", font_size=24, color=BLUE).move_to([3, 0, 2])

        self.set_camera_orientation(phi=50 * DEGREES, theta=-60 * DEGREES)
        self.play(Create(axes))
        self.add(e_curve, b_curve)
        self.play(FadeIn(e_label), FadeIn(b_label))
        self.play(t.animate.set_value(4 * PI), run_time=4, rate_func=linear)

波动

行波

axes = Axes(x_range=[-1, 10, 1], y_range=[-2, 2, 1], x_length=10, y_length=4,
             axis_config={"include_numbers": False, "stroke_width": 1})
t = ValueTracker(0)

def get_wave():
    return axes.plot(
        lambda x: np.sin(2 * x - t.get_value()),
        x_range=[-0.5, 9.5], color="#8b5cf6", stroke_width=3,
    )

wave = always_redraw(get_wave)
label = MathTex(r"y = \sin(kx - \omega t)", font_size=30, color="#06b6d4").to_edge(DOWN, buff=0.5)

self.play(Create(axes))
self.add(wave)
self.play(FadeIn(label))
self.play(t.animate.set_value(4 * PI), run_time=4, rate_func=linear)

驻波

axes = Axes(x_range=[0, 7, 1], y_range=[-2, 2, 1], x_length=10, y_length=4,
             axis_config={"include_numbers": False, "stroke_width": 1})
t = ValueTracker(0)

def get_standing():
    return axes.plot(
        lambda x: np.sin(x) * np.cos(t.get_value()),
        x_range=[0.05, 6.5], color="#8b5cf6", stroke_width=3,
    )

nodes = VGroup(*[Dot(axes.c2p(k * PI, 0), radius=0.08, color=YELLOW) for k in range(1, 3)])
wave = always_redraw(get_standing)

self.play(Create(axes), FadeIn(nodes))
self.add(wave)
self.play(t.animate.set_value(4 * PI), run_time=3, rate_func=linear)

波的干涉

s1 = Dot([-1.5, 0, 0], radius=0.1, color="#8b5cf6")
s2 = Dot([1.5, 0, 0], radius=0.1, color="#8b5cf6")

circles = VGroup()
for i in range(8):
    r = 0.5 + i * 0.5
    c1 = Circle(radius=r, color="#8b5cf6", stroke_width=1, stroke_opacity=max(0, 0.5 - i * 0.06)).move_to(s1)
    c2 = Circle(radius=r, color="#06b6d4", stroke_width=1, stroke_opacity=max(0, 0.5 - i * 0.06)).move_to(s2)
    circles.add(c1, c2)

self.play(FadeIn(s1), FadeIn(s2))
self.play(LaggedStart(*[Create(c) for c in circles], lag_ratio=0.08), run_time=2)

力学

抛体运动

ground = Line([-5, -2, 0], [5, -2, 0], color=GREY, stroke_width=2)
v0 = 5
theta = 60 * DEGREES
g = 9.8
t_max = 2 * v0 * np.sin(theta) / g
scale = 0.4

def pos(t):
    x = v0 * np.cos(theta) * t * scale - 3
    y = (v0 * np.sin(theta) * t - 0.5 * g * t**2) * scale - 2
    return [x, y, 0]

ball = Dot(pos(0), radius=0.12, color="#8b5cf6")
trail = TracedPath(lambda: ball.get_center(), stroke_color="#06b6d4", stroke_width=3)
t_tracker = ValueTracker(0)

v_arrow = always_redraw(lambda: Arrow(
    ball.get_center(),
    ball.get_center() + 0.5 * np.array([
        v0 * np.cos(theta) * scale,
        (v0 * np.sin(theta) - g * t_tracker.get_value()) * scale, 0
    ]),
    buff=0, color=YELLOW, stroke_width=3,
))

self.play(FadeIn(ground))
self.add(trail, ball, v_arrow)

n_steps = 60
for i in range(1, n_steps + 1):
    t = t_tracker.get_value() + t_max / n_steps
    self.play(ball.animate.move_to(pos(t)), t_tracker.animate.set_value(t),
              run_time=t_max / n_steps, rate_func=linear)

单摆

pivot = Dot([0, 2.5, 0], radius=0.08, color=GREY)
theta = ValueTracker(PI / 4)
length = 2.5

def get_rod():
    angle = theta.get_value()
    end = [length * np.sin(angle), 2.5 - length * np.cos(angle), 0]
    return Line(pivot.get_center(), end, color=GREY, stroke_width=2)

def get_bob():
    angle = theta.get_value()
    end = [length * np.sin(angle), 2.5 - length * np.cos(angle), 0]
    return Dot(end, radius=0.2, color="#8b5cf6")

rod = always_redraw(get_rod)
bob = always_redraw(get_bob)

self.play(FadeIn(pivot))
self.add(rod, bob)

for _ in range(4):
    self.play(theta.animate.set_value(-PI / 4), run_time=0.8, rate_func=smooth)
    self.play(theta.animate.set_value(PI / 5), run_time=0.8, rate_func=smooth)
self.play(theta.animate.set_value(0), run_time=0.5, rate_func=smooth)

弹簧振子

anchor = Dot([0, 2.5, 0], radius=0.08, color=GREY)
y_val = ValueTracker(0.5)

def get_spring():
    y = y_val.get_value()
    pts = []
    n_coils = 8
    for i in range(n_coils * 2 + 1):
        t = i / (n_coils * 2)
        py = 2.5 - t * (2.5 - y)
        px = 0.3 * (1 if i % 2 == 0 else -1) if 0 < i < n_coils * 2 else 0
        pts.append([px, py, 0])
    spring = VMobject(stroke_width=2.5, color=GREY)
    spring.set_points_as_corners([np.array(p) for p in pts])
    return spring

spring = always_redraw(get_spring)
mass = always_redraw(lambda: Square(side_length=0.5, color="#8b5cf6", fill_opacity=0.6).move_to([0, y_val.get_value(), 0]))

self.play(FadeIn(anchor))
self.add(spring, mass)
self.play(y_val.animate.set_value(1.5), run_time=0.5, rate_func=smooth)
self.play(y_val.animate.set_value(-0.5), run_time=1, rate_func=smooth)

力的分解(斜面)

plane = Polygon([-3, -2, 0], [3, -2, 0], [3, 1, 0], color=GREY, fill_opacity=0.3, stroke_width=2)
block = Square(side_length=0.6, color="#8b5cf6", fill_opacity=0.5, stroke_width=2)
block.move_to([1.5, -0.5, 0]).rotate(np.arctan(0.5))

mg = Arrow(block.get_center(), block.get_center() + DOWN * 1.5, buff=0, color=RED, stroke_width=3)
mg_label = MathTex(r"mg", font_size=24, color=RED).next_to(mg, RIGHT, buff=0.1)

normal = Arrow(block.get_center(), block.get_center() + np.array([-0.5, 1, 0]) * 1.2, buff=0, color=GREEN, stroke_width=3)
n_label = MathTex(r"N", font_size=24, color=GREEN).next_to(normal, LEFT, buff=0.1)

comp = Arrow(block.get_center(), block.get_center() + np.array([1, 0.5, 0]) * 0.8, buff=0, color="#06b6d4", stroke_width=3)
comp_label = MathTex(r"mg\sin\theta", font_size=22, color="#06b6d4").next_to(comp, UR, buff=0.1)

self.play(Create(plane))
self.play(FadeIn(block))
self.play(GrowArrow(mg), FadeIn(mg_label))
self.play(GrowArrow(normal), FadeIn(n_label))
self.play(GrowArrow(comp), FadeIn(comp_label))

轨道运动

sun = Dot([0, 0, 0], radius=0.3, color=YELLOW)
sun_glow = Dot([0, 0, 0], radius=0.5, color=YELLOW, fill_opacity=0.2)
orbit = Circle(radius=2.5, color=GREY, stroke_width=1, stroke_opacity=0.3)
planet = Dot([2.5, 0, 0], radius=0.15, color="#06b6d4")
trail = TracedPath(lambda: planet.get_center(), stroke_color="#06b6d4", stroke_width=2, stroke_opacity=0.5)

self.play(FadeIn(sun_glow), FadeIn(sun), Create(orbit))
self.add(trail, planet)
self.play(MoveAlongPath(planet, orbit), run_time=3, rate_func=linear)

热力学

气体分子运动

box = Rectangle(width=6, height=4, color=WHITE, stroke_width=2)
np.random.seed(42)
n_mol = 15
molecules = VGroup()
velocities = []
for _ in range(n_mol):
    x, y = np.random.uniform(-2.5, 2.5), np.random.uniform(-1.5, 1.5)
    vx, vy = np.random.uniform(-1, 1), np.random.uniform(-1, 1)
    d = Dot([x, y, 0], radius=0.1, color="#8b5cf6")
    molecules.add(d)
    velocities.append(np.array([vx, vy, 0]))

self.play(Create(box), FadeIn(molecules))

for _ in range(60):
    new_anims = []
    for i, mol in enumerate(molecules):
        pos = mol.get_center() + velocities[i] * 0.05
        if abs(pos[0]) > 2.8: velocities[i][0] *= -1; pos[0] = np.clip(pos[0], -2.8, 2.8)
        if abs(pos[1]) > 1.8: velocities[i][1] *= -1; pos[1] = np.clip(pos[1], -1.8, 1.8)
        new_anims.append(mol.animate.move_to(pos))
    self.play(*new_anims, run_time=0.05, rate_func=linear)

能量守恒

ke_bar = Rectangle(width=0.8, height=0.1, color="#8b5cf6", fill_opacity=0.7)
pe_bar = Rectangle(width=0.8, height=0.1, color="#06b6d4", fill_opacity=0.7)
ke_bar.move_to([-1.5, -1.5, 0], aligned_edge=DOWN)
pe_bar.move_to([1.5, -1.5, 0], aligned_edge=DOWN)

ke_label = Text("动能", font_size=20, color="#8b5cf6").next_to(ke_bar, DOWN, buff=0.15)
pe_label = Text("势能", font_size=20, color="#06b6d4").next_to(pe_bar, DOWN, buff=0.15)
total_label = MathTex(r"E_{\text{total}} = \text{const}", font_size=28, color=YELLOW).to_edge(UP, buff=0.5)

self.play(FadeIn(ke_label), FadeIn(pe_label), FadeIn(total_label))
self.add(ke_bar, pe_bar)

for ke_h, pe_h in [(2.5, 0.5), (0.5, 2.5), (2.0, 1.0), (1.0, 2.0), (2.5, 0.5)]:
    self.play(
        ke_bar.animate.stretch_to_fit_height(ke_h).move_to([-1.5, -1.5 + ke_h / 2, 0]),
        pe_bar.animate.stretch_to_fit_height(pe_h).move_to([1.5, -1.5 + pe_h / 2, 0]),
        run_time=0.8, rate_func=smooth,
    )

场景速查表

场景核心技术复杂度
电场线数值积分场线 + VMobject
磁场Arc + 循环
电磁波ThreeDScene + 双曲线
行波ValueTracker + plot
驻波ValueTracker + 节点
干涉Circle + LaggedStart
抛体ValueTracker + 轨迹
单摆ValueTracker + always_redraw
弹簧ValueTracker + zigzag VMobject
斜面Polygon + Arrow 分解
轨道Circle + MoveAlongPath
气体随机初始化 + 碰撞循环
能量stretch_to_fit_height

下一步:3b1b 源码 → 动画风格参考