Manim Updater 与 ValueTracker:让图形随参数实时变化
Updaters make Mobjects re-evaluate every frame, which is how Manim does "reactive" graphics without keyframes. The pattern: a ValueTracker holds a number; an always_redraw(lambda: ...) rebuilds a Mobject from that number each frame; playing tracker.animate.set_value(...) animates the number, and everything derived from it follows. This chapter shows the four building blocks — dt-based add_updater for per-frame logic (rotations, velocity physics), updater functions that read other objects' state, always_redraw for full re-construction, and TracedPath for comet-tail trajectories. Full examples: a dot sweeping a sine curve with a live tangent line, a circle whose radius slider drives area and circumference labels, and a tangent-vector field on a parametric curve. Also the housekeeping you must not skip: clear_updaters() before transforming an updated object, and suspending updaters while capturing static frames.
响应式思维:参数 → 图形
普通动画是"关键帧"思维(从状态 A 到状态 B),而 updater 是"响应式"思维:图形是参数的函数,动画只动参数。核心三件套:
from manim import *
class TrackerTour(Scene):
def construct(self):
axes = Axes(x_range=[0, 8, 2], y_range=[-2, 2, 1],
x_length=10, y_length=4)
curve = axes.plot(lambda x: np.sin(x), color=BLUE)
# 1) 参数(一个数)放进 ValueTracker
t = ValueTracker(0)
# 2) 图形声明为参数的函数, always_redraw 每帧重建
dot = always_redraw(
lambda: Dot(axes.c2p(t.get_value(),
np.sin(t.get_value())),
color=YELLOW))
tangent = always_redraw(
lambda: Line(
axes.c2p(t.get_value() - 0.4,
np.sin(t.get_value() - 0.4)),
axes.c2p(t.get_value() + 0.4,
np.sin(t.get_value() + 0.4)),
color=ORANGE, stroke_width=6))
self.add(axes, curve, dot, tangent)
# 3) 动画只动参数, 点和切线自动跟随
self.play(t.animate.set_value(8), run_time=5, rate_func=linear)
add_updater:每帧执行的自定义逻辑
add_updater(func) 每帧调用 func,签名是 (mob, dt),dt 是帧时长。适合连续逻辑(旋转、物理):
arrow = Arrow(ORIGIN, [1, 0, 0], buff=0, color=RED)
label = MathTex("\theta").next_to(arrow, UP)
def swing(m, dt):
m.rotate(0.8 * dt) # 每秒转 0.8 弧度
label.next_to(m, UP) # 让标签跟着箭头走
arrow.add_updater(swing)
self.add(arrow, label)
self.wait(4)
状态读取型 updater:让 B 盯着 A
dot = Dot(color=YELLOW)
dot.add_updater(lambda m: m.move_to(
np.array([2 * np.sin(0.5 * self.time),
1.5 * np.cos(0.7 * self.time), 0])))
# self.time 是场景时间, 上面是李萨如轨迹
注意写法差异:需要 dt 用双参函数,只需要每帧回调用 lambda m: ...。
TracedPath:彗星尾巴
from manim import TracedPath
dot = Dot(color=YELLOW)
path = TracedPath(dot.get_center, stroke_color=YELLOW,
stroke_width=4, fading_fraction=0.6)
dot.add_updater(lambda m: m.move_to(
np.array([2 * np.sin(self.time), 2 * np.sin(3 * self.time), 0])))
self.add(path, dot)
self.wait(5)
TracedPath 内部就是个 updater,持续记录 dot.get_center 的轨迹。fading_fraction 让尾巴渐隐,非常适合"运动轨迹"教学。
滑杆交互:一张图讲透圆的面积
r = ValueTracker(1)
circle = always_redraw(
lambda: Circle(radius=r.get_value(), color=BLUE))
formula = always_redraw(
lambda: MathTex("S = \pi r^2 = %.2f" % (np.pi * r.get_value()**2))
.to_edge(UP))
self.add(circle, formula)
self.play(r.animate.set_value(2.2), run_time=3)
self.play(r.animate.set_value(0.6), run_time=2)
一次动画改动半径,圆与面积公式同步更新——这就是 updater 的叙事威力(完整版见复刻系列)。
必须记住的清理动作
- 变换前先 clear_updaters():带 updater 的对象直接 play(Transform(...)) 会每帧被 updater 覆盖,看起来"动画没生效"。
dot.clear_updaters()后再变换。 - 暂停响应:
dot.suspend_updating()/resume_updating()用于拍"定妆照"。 - always_redraw 的对象别重复 add:它返回的就是带 updater 的普通对象,self.add 一次即可。