Manim 的 shift 和 move_to 有什么区别,什么时候用哪个?
2026 年 10 月 4 日 · 极坐标⋅XYZ
Manim 的 shift 和 move_to 有什么区别,什么时候用哪个?
shift 的参数是一段位移,物体从现在的位置出发,按这个方向和距离挪过去。move_to 的参数是一个落点,物体的中心直接放到那个点上,原来在哪都一样。所以同一行 shift 多执行几次,物体会越走越远;move_to 执行多少次都停在同一处。知道终点坐标、或者要对到另一个物体上,用 move_to;只想在现有位置上挪一点,用 shift。
Manim 是一个用 Python 写数学动画的开源引擎,最早是 Grant Sanderson 给自己的 3Blue1Brown 频道写的。图形、公式、坐标系在代码里都是对象,摆在哪、怎么动由你一行行写出来,渲染时它逐帧算好再合成视频。现在通用的是社区维护的 Manim Community,2026 年的最新版本是 0.21.0,本文的代码、坐标和报错信息都是 2026 年 10 月在这一版上跑出来的。没接触过 Manim 的话,可以先看这篇介绍。
下面三段完整代码可以在极坐标⋅XYZ 的工作区里直接运行,点代码块上面的「在工作区中打开」就行。工作区里装的也是 Manim 0.21.0,电脑上不用装 Python 和 LaTeX。
同一个 RIGHT * 2,两个结果
两个方块叠在一起,都从 (-3, 1) 出发。蓝的用 shift,黄的用 move_to,传进去的参数一模一样,都是 RIGHT * 2。同样的两行连播两遍:
from manim import *
class ShiftVsMoveTo(Scene):
def construct(self):
plane = NumberPlane(background_line_style={"stroke_opacity": 0.3})
start = Dot([-3, 1, 0], color=GREY)
a = Square(side_length=1, color=BLUE).set_fill(BLUE, 0.5).move_to([-3, 1, 0])
b = Square(side_length=1, color=YELLOW).set_fill(YELLOW, 0.5).move_to([-3, 1, 0])
self.add(plane, start, a, b)
self.wait(0.5)
# 参数一模一样,都是 RIGHT * 2
self.play(a.animate.shift(RIGHT * 2), b.animate.move_to(RIGHT * 2), run_time=2)
self.wait(0.5)
# 同样的两行再来一遍
self.play(a.animate.shift(RIGHT * 2), b.animate.move_to(RIGHT * 2), run_time=2)
self.wait()
第一遍播完,蓝方块的中心在 (-1, 1),黄方块的中心在 (2, 0)。
RIGHT * 2 在 Manim 里就是数组 [2, 0, 0]。shift 把它当成「往右 2」,蓝方块的 x 从 -3 变成 -1,y 没动。move_to 把同一个数组当成坐标 (2, 0),黄方块的中心直接落过去,连 y 也从 1 变成了 0。

第二遍,蓝方块又往右走了 2,到了 (1, 1)。黄方块已经在 (2, 0) 了,这一遍它的动画里什么也看不到。
我们另外单独试过连调三次:shift(RIGHT * 2) 的 x 是 -1、1、3,move_to([2, 1, 0]) 三次都是 (2, 1)。
![同一行代码连调三次的对比:shift(RIGHT * 2) 每次在上一次基础上再走 2,x 从 -1 到 1 再到 3;move_to([2, 1, 0]) 三次都停在 x = 2](https://jizuobiao-wasm.oss-cn-shanghai.aliyuncs.com/blog/figures/manim-shift-vs-move-to-repeat.png)
move_to 其实是用 shift 写的
翻 Manim 0.21.0 的源码,move_to 的函数体只有几行,去掉类型标注大致是这样:
def move_to(self, point_or_mobject, aligned_edge=ORIGIN, coor_mask=np.array([1, 1, 1])):
if isinstance(point_or_mobject, Mobject):
target = point_or_mobject.get_critical_point(aligned_edge)
else:
target = point_or_mobject
point_to_align = self.get_critical_point(aligned_edge)
self.shift((target - point_to_align) * coor_mask)
return self
它先算出目标点,再取自己身上的对齐点,两者一减就是要走的位移,最后把这段位移交给 shift。真正改动物体的只有 shift,它做的事是给物体的每个点都加上同一个向量。move_to 多出来的那一步,是替你把「到那儿还差多远」算了。
从这几行还能读出三件事。
对齐点默认是外接框的中心。aligned_edge 的默认值是 ORIGIN,get_critical_point(ORIGIN) 取到的是物体外接框正中间那个点,也就是 get_center()。它不一定是你直觉里的「中心」:顶点在 (-2, -2)、(0, 2)、(3, 0) 的三角形,外接框中心是 (0.5, 0),重心是 (0.33, 0)。对它 move_to(ORIGIN),落到原点上的是前者。
落点可以是另一个物体。传进来的是 Mobject 时,目标点取的是那个物体的对齐点,默认也是中心,所以 tri.move_to(circle) 的结果是两个中心重合。我们把一个三角形 move_to 到圆心在 (-2, -1) 的圆上,打印出来的中心就是 (-2, -1)。
aligned_edge 可以换对齐点。写成 LEFT,两边取的都是左边缘的中点;写成 UL,取的都是左上角。
from manim import *
class MoveToTargets(Scene):
def construct(self):
plane = NumberPlane(background_line_style={"stroke_opacity": 0.3})
box = Rectangle(width=6, height=3, color=GREY_B).move_to([1, 0.5, 0])
sq = Square(side_length=1, color=YELLOW).set_fill(YELLOW, 0.5).move_to([-5, -3, 0])
self.add(plane, box, sq)
self.wait(0.5)
# 落点是一个物体:中心对中心
self.play(sq.animate.move_to(box))
self.wait(0.5)
# 加上 aligned_edge:左边对左边
self.play(sq.animate.move_to(box, aligned_edge=LEFT))
self.wait(0.5)
# 左上角对左上角
self.play(sq.animate.move_to(box, aligned_edge=UL))
self.wait(0.5)
# 落点是坐标时,aligned_edge 指的是「自己的哪个点落上去」
self.play(sq.animate.move_to(ORIGIN, aligned_edge=DL))
self.wait()
最后一步,方块的左下角落在原点,中心在 (0.5, 0.5)。

什么时候用哪个
| 你想做的事 | 用哪个 | 写法 |
|---|---|---|
| 放到画面上一个确定的位置 | move_to |
sq.move_to([2, 1, 0]) |
| 回到画面正中 | move_to |
sq.move_to(ORIGIN) |
| 叠到另一个物体上,或者对齐它的某条边 | move_to |
sq.move_to(box, aligned_edge=LEFT) |
| 在现在的位置上挪一点 | shift |
sq.shift(UP * 0.5) |
| 摆好之后微调 | shift |
label.next_to(dot, UP).shift(RIGHT * 0.1) |
| 一组物体整体平移,内部布局不变 | 两个都行 | group.shift(LEFT * 3) 或 group.move_to(ORIGIN) |
我们自己写场景时的习惯是:物体第一次摆位置用 move_to 或者 next_to,之后的动作多半是 shift。这样改起点只动一行,后面的位移都跟着走。
最后一行提一句。对 VGroup 调 move_to,对齐的是整组的外接框中心,成员一起平移,相互位置不变。我们试过一个方块在 (-2, 0)、一个圆在 (2, 1) 的组,group.move_to(ORIGIN) 之后两者到了 (-2, -0.5) 和 (2, 0.5)。要是写成循环,对每个成员单独 move_to(ORIGIN),它们就全叠到原点上了。
常见写错的地方
在 updater 里写 shift
updater 每一帧都会被调用,shift 又是累加的,写在一起物体就一直走:
from manim import *
class ShiftInUpdater(Scene):
def construct(self):
plane = NumberPlane(background_line_style={"stroke_opacity": 0.3})
per_frame = Dot([-6, 1, 0], color=RED)
per_second = Dot([-6, 0, 0], color=GREEN)
pinned = Dot([-6, -1, 0], color=YELLOW)
per_frame.add_updater(lambda m: m.shift(RIGHT * 0.1)) # 每帧走 0.1
per_second.add_updater(lambda m, dt: m.shift(RIGHT * 2 * dt)) # 每秒走 2
pinned.add_updater(lambda m: m.move_to([-4, -1, 0])) # 每帧都摆回同一个点
self.add(plane, per_frame, per_second, pinned)
self.wait(2)
红点每帧走 0.1,走多远取决于帧率。我们用 -ql(15 帧)和 -qh(60 帧)各渲了一遍:2 秒后红点分别在 x = -2.8 和 x = 6.2,一个走了 3.2,一个走了 12.2。预览时看着正常,换成高清导出,点就快冲出画面了。
绿点的 updater 多收一个 dt,是上一帧到这一帧过了多少秒。位移乘上它,速度就是「每秒 2」,两种帧率下 2 秒后都在 x = -2 附近。想让物体匀速一直走,用这种写法。
黄点的 updater 里是 move_to,每帧摆到同一个点,它从头到尾待在 (-4, -1)。
顺带说一句,红点和黄点的 updater 不带 dt,它们在 wait 期间能动,是因为场景里有绿点那个带 dt 的 updater。我们试过把绿点那行删掉,wait(2) 之后红点还在 x = -6,黄点也没去 (-4, -1):场景里一个带 dt 的 updater 都没有时,Manim 把 wait 当成静止画面,不调 updater。这一点上一篇讲过。
上一篇讲标签跟随时在 updater 里放的 next_to 也是这个道理:位置每帧重算一遍,重算多少次都不会跑。
同一个 play 里给同一个物体写两个 .animate
想让点往右 2 的同时往上 2,这样写不行:
self.play(dot.animate.shift(RIGHT * 2), dot.animate.shift(UP * 2))
我们试下来,点最后在 (0, 2),只往上走了,往右那一个被后面的盖掉了。同一个物体的多个动作要串在一条 .animate 后面:
self.play(dot.animate.shift(RIGHT * 2).shift(UP * 2))
# 或者直接把向量加起来
self.play(dot.animate.shift(RIGHT * 2 + UP * 2))
串起来之后,.animate 是按最终位置做一次插值,点走的是从起点到终点的直线,不会先右后上拐个弯。shift 和 move_to 也可以混着串,比如 dot.animate.move_to([2, 1, 0]).shift(LEFT),终点是 (1, 1)。
move_to 的坐标少写了 z
sq.move_to([2, 1])
在 0.21.0 上这行直接报 ValueError: operands could not be broadcast together with shapes (2,) (3,)。Manim 里的点都是三维的,二维场景也要把 z 写上:[2, 1, 0]。shift([2, 1]) 报的是同一类错。写成 RIGHT * 2 + UP 可以绕开这个问题,方向常量本身就是三个数。
以为 move_to(另一个物体) 之后会一直跟着
a.move_to(b) 读的是 b 在这一行执行时的位置。我们试过在同一个 play 里让 a 去 move_to(b)、同时让 b 往上走 2:播完 a 停在 (3, 0),那是 b 出发前的位置,b 已经到了 (3, 2)。要让 a 一直贴着 b,得用 updater 每帧重摆,做法在上一篇里。
常见追问(FAQ)
Q:只想改 x 坐标,y 不动,用哪个?
A:sq.set_x(1)。也可以写 sq.move_to([1, -1, 0], coor_mask=[1, 0, 0]),coor_mask 里为 0 的轴不动。我们把一个中心在 (-3, 2) 的物体这样移,两种写法得到的都是 (1, 2)。只改 y 用 set_y。
Q:shift 能一次传几个方向吗?
A:能,sq.shift(RIGHT * 2, UP) 会把几个向量加起来,等于 sq.shift(RIGHT * 2 + UP)。
Q:shift(LEFT * 3) 里的 3 是多少像素?
A:是 Manim 的画布单位,和像素没有固定关系。默认画布高 8 个单位、宽约 14.2 个单位,渲染成 1080p 还是 4K,物体在画面里的相对位置都一样。
Q:move_to 和 next_to 怎么选?
A:move_to 是让两个物体的同一个点重合,结果是叠在一起或者贴着同一条边。next_to 是摆在另一个物体的外侧,中间留一段 buff。给图形配标签用 next_to,把高亮框套到某个物体上用 move_to。
Q:.animate.move_to(...) 走的是直线吗?
A:是。.animate 在起点和终点之间做直线插值,前面那个先右 2 再上 2 的链式写法,我们量过途中各帧,点一直在起点和终点的连线上。想沿一条指定的曲线走,用 MoveAlongPath。
shift 和 move_to 在教程《布局(上)· 描述关系不算坐标》的第一节有视频讲解,旁边的代码可以边看边改;.animate 和 updater 分别在《动画》和《ValueTracker 与 Updater》两课里。
来源:极坐标⋅XYZ · jizuobiao.xyz · 更新于 2026-10-04 · 基于 Manim Community 0.21.0