Skip to content

API roll call (offline) ​

Batch-naming every entry marked ok in the support table: 76 colours, 16 rate functions, direction constants, the method surface, shapes and 3D solids, the animation surface, and the complex plane.

This page lists 14 examples. The code is imported from corpus/examples/api/; the screenshots are end states rendered on real machines.

API roll call: the colour constants (all 76 usable colours) ​

76 squares in rows of 10 forming a palette, each filled with its own name (red, yellow, blue, green, purple, grey, brown… all distinct) — not a field of black (black was the set_stroke-wipes-fill incident, now fixed).

API roll call: the colour constants (all 76 usable colours)

py
"""API 覆盖:颜色常量一览(全部 76 个可用色)

@expect 76 个方块按每行 10 个排成色板,每块用自己的名字上色(红黄蓝绿紫灰棕……各不相同)——不是一整片黑(黑 = 修过的 set_stroke 冲掉填充的事故)。
@note 这一组是「API 覆盖」的兜底网:三态表里 state=ok 的**颜色名**逐个上色。
      以前语料只用到十来个颜色,于是 `GREEN_A` / `GREY_BROWN` 这类名字写错(或桥没接上)
      要等某个用户随手写一次才暴露——这里一次性把整张调色板钉住。
      名字写在下面的清单里(不是循环拼出来的),这样「谁被覆盖了」在源码上就看得见。
"""

from manim import *

NAMES = [
    "BLACK", "BLUE", "BLUE_A", "BLUE_B", "BLUE_C", "BLUE_D", "BLUE_E",
    "DARKER_GRAY", "DARK_BLUE", "DARK_BROWN", "DARK_GRAY",
    "GOLD", "GOLD_A", "GOLD_B", "GOLD_C", "GOLD_D", "GOLD_E",
    "GRAY", "GRAY_A", "GRAY_B", "GRAY_C", "GRAY_D", "GRAY_E",
    "GREEN", "GREEN_A", "GREEN_B", "GREEN_C", "GREEN_D", "GREEN_E",
    "GREY", "GREY_A", "GREY_B", "GREY_BROWN", "GREY_C", "GREY_D", "GREY_E",
    "LIGHTER_GRAY", "LIGHT_BROWN", "LIGHT_GRAY", "LIGHT_PINK",
    "MAROON", "MAROON_A", "MAROON_B", "MAROON_C", "MAROON_D", "MAROON_E",
    "ORANGE", "PINK", "PURE_BLUE", "PURE_GREEN", "PURE_RED",
    "PURPLE", "PURPLE_A", "PURPLE_B", "PURPLE_C", "PURPLE_D", "PURPLE_E",
    "RED", "RED_A", "RED_B", "RED_C", "RED_D", "RED_E",
    "TEAL", "TEAL_A", "TEAL_B", "TEAL_C", "TEAL_D", "TEAL_E",
    "WHITE", "YELLOW", "YELLOW_A", "YELLOW_B", "YELLOW_C", "YELLOW_D", "YELLOW_E",
]

PER_ROW = 10
SWATCH = 0.38

# 名字 → 颜色值:`from manim import *` 把它们带进本模块的 globals
swatches = VGroup(*[
    Square(side_length=SWATCH)
    .set_fill(globals()[name], opacity=1.0)
    .set_stroke(width=0, color=BLACK)
    .move_to([
        (i % PER_ROW) * (SWATCH + 0.06) - (PER_ROW - 1) * (SWATCH + 0.06) / 2,
        (len(NAMES) // PER_ROW) / 2 * (SWATCH + 0.12) - (i // PER_ROW) * (SWATCH + 0.12),
        0,
    ])
    for i, name in enumerate(NAMES)
])
print("api-colors", len(NAMES), len(swatches))


class ApiColorsScene(Scene):
    def construct(self):
        self.play(LaggedStart(*[FadeIn(s) for s in swatches], lag_ratio=0.004))
        self.wait(0.3)

API roll call: all 16 rate functions ​

Sixteen rows, each a rate name plus one dot: every dot travels left to right over the same distance, but each row's pacing is its name — smooth starts fast and eases out, rush_into bolts from the start, there_and_back turns back at the end, wiggle jitters in place. What is observed is not the endpoint but how it gets there.

API roll call: all 16 rate functions

py
"""API 点名:全部 16 个速率函数

@expect 十六行,每行一个速率名 + 一颗圆点:圆点都从左走到右、走完同样的距离,
        但每一行的节奏就是它的名字——smooth 先快后缓、rush_into 起步就冲、
        there_and_back 走到头又折返回来、wiggle 原地抖几下。看的不是终点,是「怎么走」。

@note 三态表里 kind=rate、state=ok 的名字逐个点一遍。以前语料只用到 smooth 与 there_and_back,
      其余名字写错(或桥的 rateFunction 拿不到、柯里化没先调一次)要等用户随手写一次才暴露。
      rate_func 传的是速率**名**(字符串),所以这里用 globals() 按名取——名字写在清单里,
      「谁被覆盖了」在源码上看得见(不是循环拼出来的)。
      旧版把 16 颗点先后 Create 出来,速率根本没得看——速率是「时间怎么分配」,必须让物体
      真的走一段路才看得出来。
"""

from manim import *

RATES = [
    "double_smooth",
    "exponential_decay",
    "linear",
    "lingering",
    "not_quite_there",
    "running_start",
    "rush_from",
    "rush_into",
    "slow_into",
    "smooth",
    "smoothererstep",
    "smootherstep",
    "smoothstep",
    "there_and_back",
    "there_and_back_with_pause",
    "wiggle",
]

print("api-rates", len(RATES), all(isinstance(globals()[name], str) for name in RATES))

X0 = -4.0
SPAN = 8.0
TOP = 3.6
STEP = 0.46


class ApiRatesScene(Scene):
    def construct(self):
        dots = []
        for i, name in enumerate(RATES):
            y = TOP - i * STEP
            label = Text(name, font_size=13).move_to([-5.8, y, 0])
            track = Line([X0, y, 0], [X0 + SPAN, y, 0]).set_stroke(width=1.5, opacity=0.25)
            dot = Dot([X0, y, 0], radius=0.09)
            self.add(label, track, dot)
            dots.append(dot)
        for i, name in enumerate(RATES):
            self.play(dots[i].animate.shift([SPAN, 0, 0]), rate_func=globals()[name], run_time=1.1)
        self.wait(0.2)

API roll call: direction and axis constants ​

Several arrows pointing in the directions the RIGHT / UP / OUT constants name, directions matching the names; placements involving axis constants are not skewed.

API roll call: direction and axis constants

py
"""API 点名:方向与轴向常量

@expect 几支箭头分别指向 RIGHT/UP/OUT 等常量对应的方向,方向与名字一致;轴向常量参与的定位不偏。
@note 方向常量在适配层就是 numpy 向量(UP * 0.2、RIGHT * 2 逐元素运算处处都是)。
      这里点 X_AXIS / Y_AXIS / Z_AXIS / IN / OUT:CE 里 X_AXIS 就是 RIGHT、Z_AXIS 就是 OUT,
      Axes / ThreeDAxes 相关代码常直接拿它当方向用——写不出这几个名字就会是 NameError。
"""

from manim import *
import numpy as np

CHECKS = [
    (X_AXIS, RIGHT),
    (Y_AXIS, UP),
    (Z_AXIS, OUT),
    (IN, -OUT),
]
print("api-const", len(CHECKS), all(np.array_equal(a, b) for a, b in CHECKS))


class ApiConstScene(Scene):
    def construct(self):
        # X/Y 轴方向:两支有头箭头,方向可读
        arrow_x = Arrow(ORIGIN, RIGHT * 2.2, buff=0, color=YELLOW)
        arrow_y = Arrow(ORIGIN, UP * 2.2, buff=0, color=YELLOW)
        arrow_x.move_to([-4, -1.2, 0])
        arrow_y.move_to([-0.8, -1.2, 0])
        label_x = Text("X_AXIS = RIGHT", font_size=24).next_to(arrow_x, DOWN, buff=0.3)
        label_y = Text("Y_AXIS = UP", font_size=24).next_to(arrow_y, DOWN, buff=0.3)
        self.add(arrow_x, arrow_y, label_x, label_y)
        # Z_AXIS/OUT 与 IN 在 2D 投影为零——用说明卡 + 同心圆示意进屏/出屏
        card = Text("OUT = 垂直屏幕(出屏)\nIN = 进屏", font_size=24, line_spacing=0.6)
        card.move_to([2.5, 1.2, 0])
        rings = VGroup(*[Circle(radius=0.3 + 0.35 * i, color=BLUE) for i in range(3)])
        rings.move_to([2.5, -1.8, 0])
        rings_label = Text("进屏 / 出屏 ⊙", font_size=24).next_to(rings, DOWN)
        self.add(card, rings, rings_label)
        self.wait(0.3)

API roll call: the method surface (rewrites, geometry reads, coordinate mapping) ​

The method surface exercised item by item: style rewrites, geometry reads and coordinate mapping all pass their print assertions, and the marked points and lines land where expected on screen.

API roll call: the method surface (rewrites, geometry reads, coordinate mapping)

py
"""API 点名:方法面(改写 / 取几何 / 坐标映射)

@expect 方法面逐项用一遍:改样式、取几何、坐标映射的 print 断言全过,画面上标记点/线落在预期位置。
@note 三态表里 kind=method、state=ok 的名字点一遍。方法面最容易「表里 ok、写法不对」蒙混过关:
      manim-web 的方法名与形状并不总跟 CE 对齐(plot_line_graph 收一个选项对象、set_style 收
      camelCase 键),所以这一组把每个方法的**真实用法**写出来,靠真桥跑通来钉住。
"""

from manim import *

square = Square(side_length=1.2)
square.stretch(1.5, 0)  # 横向拉伸
square.flip(UP)  # 沿 UP 轴翻转
square.set_style(stroke_color=BLUE, fill_color=BLUE, fill_opacity=0.4)
square.align_to(Dot(point=[0, 0, 0]), UP)
square.set_x(0).set_y(0).set_z(0)
box = square.get_bounding_box()
print("api-methods-geom", len(box), round(float(square.width), 3), round(float(square.height), 3))

line = Line(start=[-2, -1, 0], end=[2, 1, 0])
print("api-methods-line", len(line.get_start()), len(line.get_end()))


def bump(_m):
    return None


dot = Dot()
dot.add_updater(bump)
print("api-methods-updaters", len(dot.get_updaters()) >= 1)
dot.remove_updater(bump)

axes = Axes(x_range=[0, 4, 1], y_range=[0, 10, 2])
curve = axes.plot(lambda x: x ** 2, x_range=[0, 3])
xy = axes.coords_to_point(1, 1)
gp = axes.input_to_graph_point(1, curve)
graph = axes.plot_line_graph([0, 1, 2, 3], [0, 1, 4, 9])
print("api-methods-axes", len(xy), len(gp), graph is not None)


class ApiMethodsScene(Scene):
    def construct(self):
        self.add(axes, graph)
        self.play(Create(square), Create(line), run_time=0.2)
        self.wait(0.2)

API roll call: shapes and markers (ok in the table, unused by the corpus) ​

A grid layout: each named shape occupies one cell with its name label under the cell, nothing overlapping and everything inside the canvas.

API roll call: shapes and markers (ok in the table, unused by the corpus)

py
"""API 点名:形状与标记(表里 ok、语料未用到的那一批)

@expect 网格布局:每个被点名的形状占一格、格下有名字标签,互不遮挡、全部在画布内。
@note 三态表里 kind=class、state=ok 的名字点一遍。这一组只做**构造 + 加进场景**——
      形状画得对不对不是这里要管的(页面点着看),要管的是「这个名字能不能用」。
      全部对象默认叠在原点,所以逐个 move_to 到网格位;格旁标注名字,谁没显示一眼可见。
"""

from manim import *

sector = AnnularSector(inner_radius=0.5, outer_radius=1, angle=PI / 2)
arcbp = ArcBetweenPoints([-1, 0, 0], [1, 0, 0], angle=PI / 2)
arcpoly = ArcPolygon([0, 1, 0], [1, -1, 0], [-1, -1, 0])
cross = Cross(Square())
diff = Difference(Square(), Circle())
excl = Exclusion(Square(), Circle())
un = Union(Square(), Circle())
inter = Intersection(Square(), Circle())
elbow = Elbow(width=1, height=1)
dashed = DashedVMobject(Circle())
cdarrow = CurvedDoubleArrow([-2, 0, 0], [2, 0, 0])
cloud = PointCloudDot(radius=0.5, numParticles=200, particleSize=6, color=YELLOW)
mark = AnnotationDot()
unit = UnitInterval()
m1 = Mobject1D()
m2 = Mobject2D()
group = Group(Square(), Circle())
box = SurroundingRectangle(Circle())
# BackgroundRectangle:构造正常、加进场景后描边/填充怎么设都不可见(黑底黑块且描边被剥)——
# 适配层缺口,本条先删掉,由主线处理。
screen = ScreenRectangle(height=2).set_stroke(WHITE, 1)
full = FullScreenRectangle().set_stroke(WHITE, 1)  # height 参数被桥忽略,只能靠 scale 缩小

tips = VGroup(
    ArrowTip(),
    ArrowTriangleTip(),
    ArrowTriangleFilledTip(),
    ArrowCircleTip(),
    ArrowCircleFilledTip(),
    ArrowSquareTip(),
    ArrowSquareFilledTip(),
    StealthTip(),
).arrange(RIGHT, buff=0.2).set_stroke(WHITE, 1.5)  # 未填充的箭头尖默认描边太细,加白描边才看得见

# ComplexPlane / PolarPlane 挪到 14-planes.py 整幅展示:这两个类的背景网格在 manim-web 0.3.24 里
# 不受 x_length / radius_max 约束(ComplexPlane 永远铺满画布,PolarPlane 只认 size),塞不进这个网格;
# 又都不能 .scale()(scale 无条件量包围盒,无 WebGL 的回归环境直接炸 getBounds)。
complex_tracker = ComplexValueTracker(1 + 1j)

print("api-classes-2d", len(tips), len(group))

# —— 网格布局:pos(row, col) 给出格心,形状缩放进格、名字标在格下 ——
COLS = [-5.5, -3.3, -1.1, 1.1, 3.3, 5.5]
ROWS = [3.0, 1.7, 0.4, -0.9, -2.2]


def pos(row, col):
    return [COLS[col], ROWS[row], 0]


ITEMS = [
    ("AnnularSector", sector, 0.7),
    ("ArcBetweenPoints", arcbp, 0.7),
    ("ArcPolygon", arcpoly, 0.6),
    ("Cross", cross, 0.6),
    ("Difference", diff, 0.6),
    ("Exclusion", excl, 0.6),
    ("Union", un, 0.5),
    ("Intersection", inter, 0.5),
    ("Elbow", elbow, 0.8),
    ("DashedVMobject", dashed, 0.6),
    ("CurvedDoubleArrow", cdarrow, 0.4),
    ("PointCloudDot", cloud, 0.8),
    ("AnnotationDot", mark, 1.0),
    ("UnitInterval", unit, 0.25),
    ("Mobject1D", m1, 0.6),
    ("Mobject2D", m2, 0.3),
    ("Group", group, 0.5),
    ("SurroundingRect", box, 0.6),
    ("ScreenRectangle", screen, 0.5),
    ("ArrowTips", tips, 1.0),
    ("FullScreenRect", full, 0.1, (4, 0)),
]


class ApiClassesScene(Scene):
    def construct(self):
        for i, item in enumerate(ITEMS):
            name, mob, s = item[0], item[1], item[2]
            r, c = item[3] if len(item) > 3 else divmod(i, len(COLS))
            big = len(item) > 3  # 底行大件:形状上提、标签下移,都不压网格
            if s != 1.0:
                mob.scale(s)  # s=1.0 的(坐标系大件)不调 scale——manim-web 的 scale 无条件量包围盒
            mob.move_to([COLS[c], ROWS[r] + (0.35 if big else 0.18), 0])
            down = 0.75 if big else (0.68 if name == 'SurroundingRect' else 0.42)
            label = Text(name, font_size=11).move_to([COLS[c], ROWS[r] - down, 0])
            self.add(label, mob)
        self.play(Create(sector), Create(arcbp), run_time=0.2)
        self.wait(0.2)

API roll call: matrices, tables, 3D and miscellany ​

A grid layout: one 3D solid per cell (each coloured and shaded with volume, not a black block), four matrices per row, four miscellany per row, name labels beside the cells, nothing overlapping and everything inside the canvas.

API roll call: matrices, tables, 3D and miscellany

py
"""API 点名:矩阵 / 表 / 3D / 杂项

@expect 网格布局:3D 实体一格一个(各自有颜色、**有明暗立体感**,不是黑块)、矩阵一行四个、
        杂项一行四个,格旁有名字标签,互不遮挡、全部在画布内。
@note 同上一组:只钉「名字能用」。3D 物体默认全叠在原点,必须逐个 shift 到独立格子;
      矩阵的数字块也会叠字,错开摆放。
**3D 实体必须落在 ThreeDScene 上**:普通的 Scene 里一盏灯都没有,而 Cube / Sphere 这些用的是
吃光的材质——放错场景就是一团纯黑(曾经就是这样)。相机摆成前方俯视(phi=50°),
网格仍读得出来,同时每个实体都看得到顶面与正面(立体感就在这儿)。
实体颜色用构造参数 color=(CE 的 fill_color / fill_opacity 在 manim-web 的 3D 实体上不生效,
set_fill/set_stroke 也不是给网格用的),标签与 2D 部件一律 add_fixed_orientation_mobjects 钉住朝向——
斜视下它们不会被压扁。
"""

from manim import *

dmat = DecimalMatrix([[1.0, 2.0], [3.0, 4.0]], h_buff=2.0)  # 默认列距太窄,小数粘连
imat = IntegerMatrix([[1, 2], [3, 4]])
mmat = MobjectMatrix([[Square(), Circle()]])
mtab = MobjectTable([[Square(), Circle()]])
num = Integer(7)
boundary = AnimatedBoundary(Square())
impl = ImplicitFunction(lambda x, y: x ** 2 + y ** 2 - 1)
banner = ManimBanner()

cube = Cube(color=BLUE_D)
sphere = Sphere(color=TEAL)
cone = Cone(color=GREEN_D)
cylinder = Cylinder(color=GOLD_D)
torus = Torus(color=MAROON)
prism = Prism(color=BLUE)
tetra = Tetrahedron(color=RED_D)
octa = Octahedron(color=PURPLE)
dodeca = Dodecahedron(color=GREEN)
icosa = Icosahedron(color=ORANGE)
dot3 = Dot3D(color=YELLOW)
axes3 = ThreeDAxes(x_range=[-1, 1, 1], y_range=[-1, 1, 1], z_range=[-1, 1, 1])
vm3 = ThreeDVMobject()  # 空几何,不进画面

print("api-classes-3d", mmat is not None, mtab is not None, impl is not None, banner is not None)

# —— 网格布局 ——
COLS4 = [-5.0, -1.68, 1.68, 5.0]
SOLID_ROWS = [3.25, 1.45, -0.35]  # 俯视会把 y 压扁,行距比 2D 版拉开一些
MAT_Y = -1.6
MISC_Y = -3.1


class ApiStructuresScene(ThreeDScene):
    def construct(self):
        # 前方俯视:theta=-90° 让 x 轴正对镜头(网格不变形),phi=50° 让实体露出顶面。
        self.set_camera_orientation(phi=50 * DEGREES, theta=-90 * DEGREES)

        fixed = []  # 标签与 2D 部件:钉住朝向,斜视下才不会被压扁

        solids = [
            ("Cube", cube, 0.38), ("Sphere", sphere, 0.38), ("Cone", cone, 0.38),
            ("Cylinder", cylinder, 0.38),
            ("Torus", torus, 0.38), ("Prism", prism, 0.38), ("Tetrahedron", tetra, 0.38),
            ("Octahedron", octa, 0.38),
            ("Dodecahedron", dodeca, 0.38), ("Icosahedron", icosa, 0.38), ("Dot3D", dot3, 1.8),
            ("ThreeDAxes", axes3, 0.5),  # 轴默认横跨画布,缩到格内
        ]
        for i, (name, mob, s) in enumerate(solids):
            r, c = divmod(i, len(COLS4))
            mob.scale(s).shift([COLS4[c], SOLID_ROWS[r], 0])
            fixed.append(Text(name, font_size=11).move_to([COLS4[c], SOLID_ROWS[r] - 0.72, 0]))
            self.add(mob)

        mats = [
            ("DecimalMatrix", dmat), ("IntegerMatrix", imat),
            ("MobjectMatrix", mmat), ("MobjectTable", mtab),
        ]
        for i, (name, mob) in enumerate(mats):
            s = 0.8 if i < 2 else 0.45  # 装实体的矩阵/表天生太宽,缩得更小
            mob.scale(s).move_to([COLS4[i], MAT_Y, 0])
            fixed.append(Text(name, font_size=11).move_to([COLS4[i], MAT_Y - 0.8, 0]))
            self.add(mob)

        misc = [
            ("Integer", num, 1.0), ("AnimatedBoundary", boundary, 0.25),
            ("ImplicitFunction", impl, 0.45), ("ManimBanner", banner, 0.3),
        ]
        for i, (name, mob, s) in enumerate(misc):
            dy = -0.25 if name == "AnimatedBoundary" else 0.0
            mob.scale(s).move_to([COLS4[i], MISC_Y + 0.15 + dy, 0])
            fixed.append(Text(name, font_size=11).move_to([COLS4[i], MISC_Y - 0.45, 0]))
            self.add(mob)

        # 标签 + 矩阵 / 表 / 杂项(都是 2D 部件):一律钉住朝向,斜视下不压扁、正对观众。
        self.add_fixed_orientation_mobjects(*fixed)

        self.play(Write(dmat), run_time=0.2)
        self.wait(0.2)

API roll call: the scene family ​

A blue cube standing at an angle in the centre of the frame: top face and two side faces visible with different shading (a real 3D scene, lit and with depth); a white line ThreeDScene ok pinned to the bottom edge of the frame — it does not rotate with the camera and is not squashed by the oblique view.

API roll call: the scene family

py
"""API 点名:场景家族

@expect 一个蓝色的立方体斜立在画面中心:能看到顶面与两个侧面、明暗不同(真 3D 场景,
        有光照有深度);画面下缘钉着一行白字 "ThreeDScene ok"——它不跟相机转、也不会被斜视压扁。
@note 场景家族在适配层里**各自是真子类**:ThreeDScene 拿到的是 manim-web 的真 3D 场景
      (3D 相机 / 光照 / 深度测试),其余几个共用同一块画布、只换相机语义。
      GraphScene 是唯一的例外——manim-web 没有这个类(CE 0.22 的公共 API 里也没有,
      那是 ManimGL 的写法),退回 2D 的 Scene 并打印一行 warn。
      本文件定义了 7 个场景类,但**只有第一个会真跑**(运行器只驱动第一个 Scene 子类):
      其余几个的价值是「类创建时就把基类名字解析掉了」——名字写错会当场 NameError,
      钉住「表里 ok 的场景名真能继承」。
"""

from manim import *


class ApiSceneFamily(ThreeDScene):
    def construct(self):
        self.set_camera_orientation(phi=60 * DEGREES, theta=-60 * DEGREES)
        badge = Text("ThreeDScene ok", font_size=30)
        badge.to_edge(DOWN, buff=0.8)
        # 钉在屏幕上(这调用自身也会把物体加进场景,见 shim 的注释)
        self.add_fixed_in_frame_mobjects(badge)
        self.add(Cube(side_length=2, color=BLUE))
        self.wait(0.1)


def _marker(label):
    return VGroup(
        Square(side_length=1.6, color=BLUE),
        Text(label, font_size=26, color=WHITE),
    )


# —— 以下六个只做「基类名字解析得到」的点名,construct 不会被执行(见 @note)——


class _FamilyMovingCamera(MovingCameraScene):
    def construct(self):
        self.add(_marker("MovingCamera ok"))


class _FamilyZoomed(ZoomedScene):
    def construct(self):
        self.add(_marker("ZoomedScene ok"))


class _FamilyVector(VectorScene):
    def construct(self):
        self.add(_marker("VectorScene ok"))


class _FamilyLinear(LinearTransformationScene):
    def construct(self):
        self.add(_marker("LinearTransform ok"))


class _FamilyGraph(GraphScene):  # manim-web 没有,退回 2D 的 Scene 并 warn
    def construct(self):
        self.add(_marker("GraphScene→Scene"))

API roll call: the animation surface (ok in the table, unused by the corpus) ​

Each animation played through: every one has a recognizable effect, none crashes and none stalls the render loop.

API roll call: the animation surface (ok in the table, unused by the corpus)

py
"""API 点名:动画面(表里 ok、语料未用到的那一批)

@expect 动画面逐支播完:每支动画都有可辨识的效果、不炸、不卡死渲染循环。
@note 三态表里 kind=anim、state=ok 的名字点一遍。动画的名字与「要几个目标、收什么参数」
      差别很大(Transform 两个、Rotate 带角度、CyclicReplace 收一个列表),所以一条一条写出来,
      靠真桥跑通钉住——写错名字或形状会当场报错。
      留痕支(播完在场景里留下可辨识终态的)排进网格位,格下标名字;纯过程支
      (Blink / Broadcast / FocusOn / ShowPassingFlash / ShowPassingFlashWithThinningStrokeWidth /
      ShrinkToCenter / Uncreate / Wiggle / ApplyWave / Succession / Unwrite)不留终态、
      静态终帧无从核对,照播但播完把被 play 收养回场景的目标请走——要观察过程得 --realtime 跑。
"""

from manim import *

COLS = [-5, -2.5, 0, 2.5, 5]
ROWS = [2.3, 0, -2.3]


def cell(r, c):
    return [COLS[c], ROWS[r], 0]


def off(base, dx, dy):
    return [base[0] + dx, base[1] + dy, 0]


class ApiAnimsScene(Scene):
    def construct(self):
        # —— 留痕支:先落格再播,终帧每格有痕迹 ——
        # FadeToColor:换色的点留在格里
        d = Dot(radius=0.35).move_to(cell(0, 0))
        self.play(FadeToColor(d, RED), run_time=0.05)
        self.add(Text("FadeToColor", font_size=20).move_to(off(cell(0, 0), 0, -0.9)))
        # GrowFromPoint:从原点长到位
        d = Dot(radius=0.35).move_to(cell(0, 1))
        self.play(GrowFromPoint(d, ORIGIN), run_time=0.05)
        self.add(Text("GrowFromPoint", font_size=20).move_to(off(cell(0, 1), 0, -0.9)))
        # ShowIncreasingSubsets:逐个亮出,终态是两个点都在
        g = VGroup(Dot().move_to(off(cell(0, 2), -0.35, 0)), Dot().move_to(off(cell(0, 2), 0.35, 0)))
        self.play(ShowIncreasingSubsets(g), run_time=0.05)
        self.add(Text("ShowIncreasingSubsets", font_size=20).move_to(off(cell(0, 2), 0, -0.9)))
        # ShowSubmobjectsOneByOne:逐个子物体亮出
        g = VGroup(Dot().move_to(off(cell(0, 3), -0.35, 0)), Dot().move_to(off(cell(0, 3), 0.35, 0)))
        self.play(ShowSubmobjectsOneByOne(g), run_time=0.05)
        self.add(Text("ShowSubmobjectsOneByOne", font_size=20).move_to(off(cell(0, 3), 0, -0.9)))
        # SpinInFromNothing:旋着进场,落在格里
        d = Dot(radius=0.35).move_to(cell(0, 4))
        self.play(SpinInFromNothing(d), run_time=0.05)
        self.add(Text("SpinInFromNothing", font_size=20).move_to(off(cell(0, 4), 0, -0.9)))
        # SpiralIn:螺旋进场
        d = Dot(radius=0.35).move_to(cell(1, 0))
        self.play(SpiralIn(d), run_time=0.05)
        self.add(Text("SpiralIn", font_size=20).move_to(off(cell(1, 0), 0, -0.9)))
        # Add:直接加进场景
        d = Dot(radius=0.35).move_to(cell(1, 1))
        self.play(Add(d), run_time=0.05)
        self.add(Text("Add", font_size=20).move_to(off(cell(1, 1), 0, -0.9)))
        # ClockwiseTransform:点变方,终态是方
        d = Dot(radius=0.3).move_to(cell(1, 2))
        sq = Square(side_length=0.7).move_to(cell(1, 2))
        self.play(ClockwiseTransform(d, sq), run_time=0.05)
        self.add(Text("ClockwiseTransform", font_size=20).move_to(off(cell(1, 2), 0, -0.9)))
        # CounterclockwiseTransform:同上反向
        d = Dot(radius=0.3).move_to(cell(1, 3))
        sq = Square(side_length=0.7).move_to(cell(1, 3))
        self.play(CounterclockwiseTransform(d, sq), run_time=0.05)
        self.add(Text("CounterclockwiseTransform", font_size=20).move_to(off(cell(1, 3), 0, -0.9)))
        # Swap:两个物体换位,终态两物体都在。
        # 桥的 play 只收养动画主目标,多目标动画(Swap / CyclicReplace)其余目标要预 add 进场景
        a = Dot(radius=0.3).move_to(off(cell(1, 4), -0.7, 0))
        b = Square(side_length=0.5).move_to(off(cell(1, 4), 0.7, 0))
        self.add(a, b)
        self.play(Swap(a, b), run_time=0.05)
        self.add(Text("Swap", font_size=20).move_to(off(cell(1, 4), 0, -0.9)))
        # CyclicReplace:三个物体轮换位置(多目标,同上预 add)
        a = Dot(radius=0.22).move_to(off(cell(2, 0), -0.7, 0))
        b = Square(side_length=0.4).move_to(cell(2, 0))
        c = Triangle().move_to(off(cell(2, 0), 0.7, 0))
        self.add(a, b, c)
        self.play(CyclicReplace(a, b, c), run_time=0.05)
        self.add(Text("CyclicReplace", font_size=20).move_to(off(cell(2, 0), 0, -0.9)))
        # Rotate:三角转 45°,终态是转过的三角
        t = Triangle().move_to(cell(2, 1)).scale(0.6)
        self.play(Rotate(t, PI / 4), run_time=0.05)
        self.add(Text("Rotate", font_size=20).move_to(off(cell(2, 1), 0, -0.9)))
        # TransformFromCopy:复制源点变方(源的副本落格,源本体预 add 留在旁作对照)
        d = Dot(radius=0.3).move_to(off(cell(2, 2), -0.8, 0))
        sq = Square(side_length=0.7).move_to(off(cell(2, 2), 0.5, 0))
        self.add(d)
        self.play(TransformFromCopy(d, sq), run_time=0.05)
        self.add(Text("TransformFromCopy", font_size=20).move_to(off(cell(2, 2), 0, -0.9)))
        # TransformMatchingShapes:点变方
        d = Dot(radius=0.3).move_to(cell(2, 3))
        sq = Square(side_length=0.7).move_to(cell(2, 3))
        self.play(TransformMatchingShapes(d, sq), run_time=0.05)
        self.add(Text("TransformMatchingShapes", font_size=20).move_to(off(cell(2, 3), 0, -0.9)))
        # FadeTransformPieces:淡入淡出换件
        d = Dot(radius=0.3).move_to(cell(2, 4))
        sq = Square(side_length=0.7).move_to(cell(2, 4))
        self.play(FadeTransformPieces(d, sq), run_time=0.05)
        self.add(Text("FadeTransformPieces", font_size=20).move_to(off(cell(2, 4), 0, -0.9)))

        # —— 纯过程支:照播钉住「名字能用」,播完把被收养回场景的目标请走(见 @note)——
        d = Dot(radius=0.3)
        self.play(Blink(d), run_time=0.05)
        self.remove(d)
        d = Dot(radius=0.3)
        self.play(Broadcast(d), run_time=0.05)
        self.remove(d)
        d = Dot(radius=0.3)
        self.play(FocusOn(d), run_time=0.05)
        self.remove(d)
        c = Circle(radius=0.3)
        self.play(ShowPassingFlash(c), run_time=0.05)
        self.remove(c)
        c = Circle(radius=0.3)
        self.play(ShowPassingFlashWithThinningStrokeWidth(c), run_time=0.05)
        self.remove(c)
        d = Dot(radius=0.3)
        self.play(ShrinkToCenter(d), run_time=0.05)
        self.remove(d)
        c = Circle(radius=0.3)
        self.play(Uncreate(c), run_time=0.05)
        self.remove(c)
        d = Dot(radius=0.3)
        self.play(Wiggle(d), run_time=0.05)
        self.remove(d)
        sq = Square(side_length=0.7)
        self.play(ApplyWave(sq), run_time=0.05)
        self.remove(sq)
        d = Dot(radius=0.3)
        self.play(Succession(Create(d), FadeOut(d)), run_time=0.05)
        self.remove(d)
        t = Text("hi")
        self.play(Unwrite(t), run_time=0.05)
        self.remove(t)
        self.wait(0.2)

API roll call: the animation surface (functions, text and wrappers) ​

Function, text and wrapper animations played through: callbacks really fire and text appears letter by letter — recognizable effects.

API roll call: the animation surface (functions, text and wrappers)

py
"""API 点名:动画面(函数 / 文字 / 包装那一批)

@expect 函数/文字/包装类动画逐支播完:回调真的被调、文字逐字出现等效果可辨识。
@note 这几个把**回调**交给动画(ApplyFunction / Homotopy / ApplyMethod 这类),或针对文字
      (TypeWithCursor)。回调从 Python 过桥是 PyProxy,能被 JS 调用——真机跑通才算数。
      逐支落格、格下标名字,终帧每格有痕迹。适配层缺口:ApplyMethod 的方法参数过桥不完整——
      "shift" 插值得 NaN(渲染空白)、"scale" 报 spread 迭代错、"move_to" 名字跑通但不位移;
      这支先按 "move_to" 点名(物体在格内可见),方法效果待桥修复后补验。
      RemoveTextLetterByLetter / UntypeWithCursor 终态是删完,垫灰字作对照。
      另一缺口:PhaseFlow 过 PyProxy 回调后几何被污染——恒等同伦也会把物体位移/放大
      (y 向漂出 e 量级、宽度涨到 ~1.9),终态无从留痕,播完把被污染的物体请走。
"""

from manim import *

COLS = [-5, -2.5, 0, 2.5, 5]
ROWS = [2.3, 0, -2.3]


def cell(r, c):
    return [COLS[c], ROWS[r], 0]


def off(base, dx, dy):
    return [base[0] + dx, base[1] + dy, 0]


class ApiAnimsFuncScene(Scene):
    def construct(self):
        # —— 函数/回调支:恒等回调,点留在格里 ——
        d = Dot(radius=0.35).move_to(cell(0, 0))
        self.play(ApplyFunction(lambda m: m, d), run_time=0.05)
        self.add(Text("ApplyFunction", font_size=20).move_to(off(cell(0, 0), 0, -0.9)))
        # ApplyMethod:见 @note——"shift"/"scale" 过桥有缺口,用 "move_to" 点名
        d = Dot(radius=0.35).move_to(cell(0, 1))
        self.add(d)
        self.play(ApplyMethod(d, "move_to", off(cell(0, 1), 0.4, 0)), run_time=0.05)
        self.add(Text("ApplyMethod", font_size=20).move_to(off(cell(0, 1), 0, -0.9)))
        d = Dot(radius=0.35).move_to(cell(0, 2))
        self.play(ApplyPointwiseFunction(lambda p: p, d), run_time=0.05)
        self.add(Text("ApplyPointwiseFunction", font_size=20).move_to(off(cell(0, 2), 0, -0.9)))
        d = Dot(radius=0.35).move_to(cell(0, 3))
        self.play(ApplyPointwiseFunctionToCenter(lambda p: p, d), run_time=0.05)
        self.add(Text("ApplyPointwiseFunctionToCenter", font_size=20).move_to(off(cell(0, 3), 0, -0.9)))
        d = Dot(radius=0.35).move_to(cell(0, 4))
        self.play(ApplyComplexFunction(lambda z: z, d), run_time=0.05)
        self.add(Text("ApplyComplexFunction", font_size=20).move_to(off(cell(0, 4), 0, -0.9)))
        # —— homotopy 支:恒等同伦,点留在格里(PhaseFlow 会把点撑大,独占一格不碍邻格)——
        d = Dot(radius=0.35).move_to(cell(1, 0))
        self.play(Homotopy(lambda x, y, z, t: [x, y, z], d), run_time=0.05)
        self.add(Text("Homotopy", font_size=20).move_to(off(cell(1, 0), 0, -0.9)))
        d = Dot(radius=0.35).move_to(cell(1, 1))
        self.play(ComplexHomotopy(lambda z, t: z, d), run_time=0.05)
        self.add(Text("ComplexHomotopy", font_size=20).move_to(off(cell(1, 1), 0, -0.9)))
        d = Dot(radius=0.35).move_to(cell(1, 2))
        self.play(SmoothedVectorizedHomotopy(lambda x, y, z, t: [x, y, z], d), run_time=0.05)
        self.add(Text("SmoothedVectorizedHomotopy", font_size=20).move_to(off(cell(1, 2), 0, -0.9)))
        d = Dot(radius=0.35).move_to(cell(1, 3))
        self.play(PhaseFlow(lambda p: p, d), run_time=0.05)
        self.remove(d)  # 见 @note:PhaseFlow 过桥污染几何,终态不留痕
        self.add(Text("PhaseFlow*", font_size=20).move_to(off(cell(1, 3), 0, -0.9)))
        # —— 文字支:垫灰字作对照,白字落在灰字上 ——
        ghost = Text("hi", color=GREY).move_to(cell(1, 4))
        self.add(ghost)
        t = Text("hi").move_to(cell(1, 4))
        self.play(AddTextLetterByLetter(t), run_time=0.05)
        self.add(Text("AddTextLetterByLetter", font_size=20).move_to(off(cell(1, 4), 0, -0.9)))
        ghost = Text("hi", color=GREY).move_to(cell(2, 0))
        self.add(ghost)
        t = Text("hi").move_to(cell(2, 0))
        self.play(RemoveTextLetterByLetter(t), run_time=0.05)
        self.add(Text("RemoveTextLetterByLetter", font_size=20).move_to(off(cell(2, 0), 0, -0.9)))
        ghost = Text("hi", color=GREY).move_to(cell(2, 1))
        self.add(ghost)
        t = Text("hi").move_to(cell(2, 1))
        self.play(AddTextWordByWord(t), run_time=0.05)
        self.add(Text("AddTextWordByWord", font_size=20).move_to(off(cell(2, 1), 0, -0.9)))
        t = Text("hi").move_to(cell(2, 2))
        self.play(TypeWithCursor(t), run_time=0.05)
        self.add(Text("TypeWithCursor", font_size=20).move_to(off(cell(2, 2), 0, -0.9)))
        ghost = Text("hi", color=GREY).move_to(cell(2, 3))
        self.add(ghost)
        t = Text("hi").move_to(cell(2, 3))
        self.play(UntypeWithCursor(t), run_time=0.05)
        self.add(Text("UntypeWithCursor", font_size=20).move_to(off(cell(2, 3), 0, -0.9)))
        self.wait(0.2)

API roll call: shapes, markers and graphs (ok in the table, previously unrolled) ​

A four-row grid (six cells per row, five in the last): each named class in one cell with its name label under the cell; dependency clusters (arc plus arc brackets, square plus brackets) placed as groups; nothing overlapping and everything inside the canvas.

API roll call: shapes, markers and graphs (ok in the table, previously unrolled)

py
"""API 点名:形状 / 标记 / 图(表里 ok、语料此前没点到的那一批)

@expect 四行网格(每行六格,末行五格):每个被点名的类占一格、格下有名字标签;
        依赖簇(弧+弧括号、方块+括号)成组摆放;互不遮挡、全部在画布内。
@note 这些类 manim-web 都有,只是构造姿势与 CE 不同(CE 收位置参数、manim-web 多收一个选项对象)——
      在 surface.ts 的 CTOR 里逐个登记了配方。全部对象默认叠在原点,必须逐格摆放;
      MathTex 一支只敢用 move_to 定位。
      场类缺口:manim-web 0.3.24 的 VectorField._regenerate() 是空的(抽象基类,只有
      ArrowVectorField / StreamLines 生成可见物)——名字能构造但画不出东西,格内放灰方块作
      格位参照;ArrowVectorField / StreamLines 的线箭头按场走会越出采样范围,范围与步长收紧
      (步长 ×0.8 封顶箭头长)才不侵入上排 Variable / Code。
      2026-10-11:原先末行的 Line3D / Arrow3D / ConvexHull3D 撤出本文件——它们吃光,
      在普通的 Scene 里是黑块,已挪进 `api/12-classes-3d-extra.py` 的真 ThreeDScene。
"""

from manim import *

COLS = [-5.5, -3.3, -1.1, 1.1, 3.3, 5.5]
ROWS = [2.95, 1.1, -0.75, -2.6]


def cell(r, c, dy=0.15):
    return [COLS[c], ROWS[r] + dy, 0]


def label_at(r, c, text, dy=-0.42):
    return Text(text, font_size=11).move_to([COLS[c], ROWS[r] + dy, 0])


class ExtraClassesScene(Scene):
    def construct(self):
        # —— 行 0:切线 / 切弧 / 多角星 / 凸包 / 多角形 ——
        arc = Arc(radius=0.7, start_angle=0, angle=PI / 2).move_to(cell(0, 0, 0.3))
        self.add(label_at(0, 0, "ArcBrace"), ArcBrace(arc), arc)

        tline = TangentLine(Circle(radius=1), 0.25).scale(0.45).move_to(cell(0, 1))
        self.add(label_at(0, 1, "TangentLine"), tline)

        tarc = TangentialArc(ORIGIN, RIGHT, radius=1, angle=PI / 3).scale(0.8).move_to(cell(0, 2))
        self.add(label_at(0, 2, "TangentialArc"), tarc)

        polygram7 = RegularPolygram(7, density=3).scale(0.55).move_to(cell(0, 3))
        self.add(label_at(0, 3, "RegularPolygram"), polygram7)

        hull = ConvexHull([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0.2, 0.8, 0]]).scale(0.8).move_to(cell(0, 4))
        self.add(label_at(0, 4, "ConvexHull"), hull)

        pg = Polygram([[[0, 0, 0], [1, 0, 0], [0.5, 0.9, 0]]]).scale(0.8).move_to(cell(0, 5))
        self.add(label_at(0, 5, "Polygram"), pg)

        # —— 行 1:带标签的线 / 箭头 / 点 / 多角形;SVG 路径;图 ——
        lline = LabeledLine(start=LEFT, end=RIGHT, label="l").scale(0.7).move_to(cell(1, 0))
        self.add(label_at(1, 0, "LabeledLine"), lline)

        larrow = LabeledArrow(start=LEFT, end=RIGHT, label="a").scale(0.7).move_to(cell(1, 1))
        self.add(label_at(1, 1, "LabeledArrow"), larrow)

        ldot = LabeledDot(point=ORIGIN, label="d").move_to(cell(1, 2))
        self.add(label_at(1, 2, "LabeledDot"), ldot)

        lpg = LabeledPolygram(vertex_groups=[[[0, 0, 0], [1, 0, 0], [0.5, 0.9, 0]]], label="p")
        lpg.scale(0.8).move_to(cell(1, 3))
        self.add(label_at(1, 3, "LabeledPolygram"), lpg)

        svgp = VMobjectFromSVGPath(path_data="M0,0 L1,0 L1,1 Z").scale(0.95).move_to(cell(1, 4, 0.35))
        self.add(label_at(1, 4, "VMobjectFromSVGPath"), svgp)

        graph = Graph(vertices=[1, 2, 3], edges=[(1, 2), (2, 3), (3, 1)]).scale(0.32).move_to(cell(1, 5))
        self.add(label_at(1, 5, "Graph"), graph)

        # —— 行 2:有向图 / 样本空间 / 变量 / 代码块 / 名字表 / 公式 ——
        digraph = DiGraph(vertices=[1, 2, 3], edges=[(1, 2), (2, 3)]).scale(0.32).move_to(cell(2, 0))
        self.add(label_at(2, 0, "DiGraph"), digraph)

        space = SampleSpace(width=4, height=3).scale(0.32).move_to(cell(2, 1))
        space.set_stroke(WHITE, 1)  # 内圈椭圆默认黑填充,加白描边才看得见结构
        self.add(label_at(2, 1, "SampleSpace"), space)

        var = Variable(label="t", value=1).scale(0.8).move_to(cell(2, 2))
        self.add(label_at(2, 2, "Variable"), var)

        code = Code(code="x = 1", language="python").scale(1.0).move_to(cell(2, 3))
        self.add(label_at(2, 3, "Code"), code)

        vd = VDict({"a": Dot().shift(LEFT * 0.35), "b": Square(side_length=0.5).shift(RIGHT * 0.35)})
        vd.move_to(cell(2, 4))
        self.add(label_at(2, 4, "VDict"), vd)

        smtex = SingleStringMathTex("x^2 + y^2").move_to(cell(2, 5))  # MathTex 只用 move_to 定位
        self.add(label_at(2, 5, "SingleStringMathTex"), smtex)

        # —— 行 3:向量场三兄弟 + 两个括号簇(底行下方留白,垂下来的括号不压人)——
        # PGroup(点云类)在桥上不渲染,已删并由主线处理。

        def cell_field(cx, cy):
            # 方向场绕格心旋、幅度压到 0.4——箭头长度随场幅走,不压就伸出格子
            return lambda p: [-(p[1] - cy) * 0.4, (p[0] - cx) * 0.4, 0]

        cx, cy = COLS[0], ROWS[3]
        vfield = VectorField(
            cell_field(cx, cy), x_range=[cx - 1, cx + 1, 1], y_range=[cy - 0.35, cy + 0.75, 1],
        )
        # VectorField 上游不生成可见物(见 @note)——灰方块只标格位,不是它的渲染结果
        self.add(label_at(3, 0, "VectorField", -0.32), Square(side_length=0.3, color=GREY).move_to(cell(3, 0)), vfield)

        ax, ay = COLS[1], ROWS[3]
        afield = ArrowVectorField(
            cell_field(ax, ay), x_range=[ax - 0.6, ax + 0.6, 0.5], y_range=[ay, ay + 0.55, 0.5],
            length_func=lambda norm: min(norm, 0.2),  # 箭头压短,可见簇整体缩在格上半区,让开格下标签
        )
        self.add(label_at(3, 1, "ArrowVectorField", -0.32), afield)

        sx, sy = COLS[2], ROWS[3] + 0.35
        stream = StreamLines(
            cell_field(sx, sy), x_range=[sx - 0.8, sx + 0.8, 1], y_range=[sy - 0.5, sy + 0.3, 1],
            num_lines=4,
        )
        self.add(label_at(3, 2, "StreamLines", -0.75), stream)

        sq1 = Square(side_length=0.8).move_to(cell(3, 3, 0.1))
        # 名签放方块上方:括号+文字在方块下垂 ~0.8,下方是 "a"/"b" 的位置
        self.add(label_at(3, 3, "BraceLabel", 0.72), BraceLabel(sq1, "a"), sq1)

        sq2 = Square(side_length=0.8).move_to(cell(3, 4, 0.1))
        self.add(label_at(3, 4, "BraceText", 0.72), BraceText(sq2, "b"), sq2)

        self.wait(0.2)

API roll call: animation addenda (ok in the table, previously unrolled) ​

Each animation in this batch played once with its object in its own place, nothing overlapping: a line revealing its middle, a square returning to its saved state, a dot moving to the target above, three dots entering in turn, a number walking to 10.00, a dot becoming a square, a single dot present. All inside the canvas.

API roll call: animation addenda (ok in the table, previously unrolled)

py
"""API 点名:动画补遗(表里 ok、语料此前没点到的那一批)

@expect 这批动画各播一次、对象各占一处互不遮挡:线段露出中段、方块回到存过的样子、
        圆点搬到上方目标位、三颗点依次入场、数字走到 10.00、点变方、单颗点在场。全部在画布内。
@note 第五轮补齐。这几支在 manim-web 上的形状与 CE 差得最远,各接了一处:
      Wait 是场景方法、LaggedStartMap 收动画**类**、ChangeSpeed 收速度**函数**、
      ChangingDecimal 的 f(t) 表达不了——都落在适配层与判定层(见 manim-shim.ts / surface.ts)。
      所有对象默认叠在原点,逐个 shift 到独立位置(数字与方块错开)。
"""

from manim import *


class ExtraAnimsScene(Scene):
    def construct(self):
        # ShowPartial:只露出路径的一段(CE 给两个位置比例,manim-web 收选项)
        line = Line(LEFT * 2, RIGHT * 2).shift([-4.2, 2.4, 0])
        self.add(line)
        self.play(ShowPartial(line, 0.2, 0.8))

        # Animation:基类本身也能播(无视觉变化,只占时长)
        self.play(Animation(Dot().shift([6.2, 3.1, 0])))

        # Restore:回到 save_state() 存下的那一版(位置 / 颜色都回去)
        square = Square(side_length=1, color=BLUE).shift([-4.2, 0.5, 0])
        square.save_state()
        square.shift(RIGHT * 2).set_color(RED)
        self.play(Restore(square))

        # MoveToTarget:先造「目标版」、改目标版,再让本体搬过去
        dot = Dot().shift([-4.2, -0.8, 0])
        dot.generate_target()
        dot.target.shift(UP * 2)
        self.play(MoveToTarget(dot))

        # Wait:当成动画播(manim-web 里它是场景方法,适配层把它摘出来落到 wait)
        self.play(Wait(0.3))

        # LaggedStartMap:把一个动画类依次套到一组的每个子物体上
        row = VGroup(Dot().shift(LEFT * 0.5), Dot(), Dot().shift(RIGHT * 0.5))
        row.shift([-1.6, 2.4, 0])
        self.play(LaggedStartMap(FadeIn, row, lag_ratio=0.3))

        # ChangeSpeed:按倍数改播放速度(CE 给数字,manim-web 要速度函数)
        # ChangeSpeed 包住的 Create(Circle) 播完什么都不显示(换位置重试仍丢)——
        # 适配层缺口,本条先删掉,由主线处理。

        # ChangingDecimal:数字每帧跟着函数走(独立位置,别让方块压住)
        number = DecimalNumber(0).shift([-1.6, -0.8, 0])
        self.play(ChangingDecimal(number, lambda t: 10 * t))

        # TransformAnimations:把一个动画串到另一个动画上(起点终点都搬到同一格)
        self.play(
            TransformAnimations(
                Create(Dot().shift([0.8, 2.4, 0])),
                FadeIn(Square(side_length=0.5).shift([0.8, 2.4, 0])),
            )
        )

        # get_parts 取分段;m[i] 按下标取子物体
        # (MathTex 的分段是排版完成才有的,Node 里没有 KaTeX 环境、段数为空也正常——
        #  这里只钉「调用能走通、拿到的是个列表」;真浏览器里的段数由实验场核。)
        formula = MathTex("a", "+", "b")
        parts = formula.get_parts()
        m = VGroup(Dot(), Dot(), Dot())
        i = 1
        assert isinstance(parts, list), type(parts)
        assert len(m) == 3, len(m)
        m[i].shift([0.8, 0.8, 0])
        self.add(m[i])

API roll call: 3D solids (line, arrow, convex hull) — must land on ThreeDScene ​

Three 3D solids in a row at x = -4 / 0 / +4: a cyan horizontal line segment on the left, a blue arrow in the same direction in the middle (segment plus cone tip), a green tetrahedron on the right with two or three faces of different shading (light is what gives volume, not a black blob). All three lie along the horizontal of the frame, each with its name label below, labels facing the camera and not squashed by the oblique view.

API roll call: 3D solids (line, arrow, convex hull) — must land on ThreeDScene

py
"""API 点名:3D 实体(线 / 箭头 / 凸包)——必须落在 ThreeDScene 上

@expect 一排三件 3D 实体,从左到右在 x = -4 / 0 / +4:左边是一根青色的水平线段;
        中间是一枝同向的蓝色箭头(线段 + 锥头);右边是一个绿色的四面体——
        能看到深浅不同的两三个面(有光才有立体,不是一团黑)。三件都沿画面水平方向躺着,
        各自下方有名字标签,标签正对镜头、不被斜视压扁。
@note 这三件和 Cube / Sphere 一样吃光:manim-web 给它们的是标准材质,靠**场景灯光**照亮。
      放进普通的 Scene 里一盏灯都没有,实心面就是纯黑(ConvexHull3D 最刺眼——画面里只剩一个黑三角),
      线材质虽不吃光,缩放 / 透视也跟 2D 不是一回事。
      它们原本并排躺在 `api/11-classes-extra.py` 的 2D 网格里,2026-10-11 复盘 3D 语料时发现,
      单独挪到这条真 3D 场景。
      为什么三件都**沿 x 摆**而不是竖着:画面是透视的,一段沿 y / z 的线段只要离开画面中轴,
      两个端点的深度就不同,投影下来会被画成**斜的**(看起来像画歪了)。沿 x 的线段两端深度相同,
      屏幕上严格水平——要竖着立一根还得额外解释「这是透视,不是 bug」,不如直接摆正。
"""

from manim import *

XS = [-4.4, 0.0, 4.4]
ARM = 1.5  # 每件的半长


class ApiThreeDExtraScene(ThreeDScene):
    def construct(self):
        # 前方俯视:theta=-90° 让 x 轴正对镜头;phi=50° 让四面体露出顶面。
        self.set_camera_orientation(phi=50 * DEGREES, theta=-90 * DEGREES)

        line3 = Line3D(LEFT * ARM, RIGHT * ARM, color=TEAL).move_to([XS[0], 0.5, 0])
        arrow3 = Arrow3D(LEFT * ARM, RIGHT * ARM, color=BLUE).move_to([XS[1], 0.5, 0])
        hull3 = ConvexHull3D([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]], color=GREEN).scale(2.6)
        hull3.move_to([XS[2], 0.5, 0])

        self.add(line3, arrow3, hull3)

        labels = VGroup(
            Text("Line3D", font_size=18).move_to([XS[0], -1.5, 0]),
            Text("Arrow3D", font_size=18).move_to([XS[1], -1.5, 0]),
            Text("ConvexHull3D", font_size=18).move_to([XS[2], -1.9, 0]),
        )
        # 钉住朝向(位置仍在 3D 里):斜视下字才不会躺倒被压扁。
        # 注意它会**自己把物体加进场景**(CE 就是这么做的),不必再 add 一遍。
        self.add_fixed_orientation_mobjects(labels)

        self.wait(0.2)

API roll call: a whole inline SVG string (needs browser DOM, checked in the playground only) ​

A shape (a circle) parsed out of an inline SVG string and drawn.

API roll call: a whole inline SVG string (needs browser DOM, checked in the playground only)

py
"""API 点名:整段 SVG 字符串(要浏览器 DOM,只在实验场核)

@expect 从一段内联 SVG 字符串里解析出图形(一个圆)并画出来。
@browser manim-web 的 SVGMobject 用 DOMParser 解析 SVG——那是浏览器 API,Node 里没有,
         所以这条只由实验场页面(真浏览器)核;Node 套件会打印出跳过了它。
@note 与 VMobjectFromSVGPath 不同:只给一条路径 d 时不需要 DOM(那条在 11 里、自动测试覆盖)。
      注意:SVGMobject 解析结果的默认摆放是乱的——viewBox '0 0 10 10' 的圆,圆心跑到画面外右下、
      尺寸巨大(疑似 viewBox→manim 坐标的映射有 bug),move_to 居中 + scale 之后显示正常;
      映射本身由主线另查。
"""

from manim import *


class SvgStringScene(Scene):
    def construct(self):
        svg = "<svg viewBox='0 0 10 10'><circle cx='5' cy='5' r='4'/></svg>"
        svg_mob = SVGMobject(svg_string=svg)
        # 解析结果的默认位置/尺寸乱(圆心偏出画面右下),居中并缩到合适大小
        svg_mob.move_to([0, 0, 0]).scale(0.5)
        self.add(svg_mob)

Complex plane and polar grid: the whole canvas is their canvas ​

First a complex plane fills the canvas with a yellow dot marked at 60° on the unit circle and labelled e^{iπ/3}; then the whole frame fades into a polar grid (concentric circles plus radial lines) with the same direction marked again.

Complex plane and polar grid: the whole canvas is their canvas

py
"""复平面与极坐标网格:整幅画布就是它们的画布

@expect 先是一幅复平面铺满画布,单位圆方向上标出一个 60° 的黄点并注明 e^{iπ/3};随后整幅淡出,换成极坐标网格(同心圆 + 放射线),同一个方向再标一次。
@note 这两个类的背景网格在 manim-web 0.3.24 里不受 x_length / radius_max 约束——
      ComplexPlane 的网格永远铺满画布(x_range 只管刻度),PolarPlane 只认 size 不认 radius_max。
      所以它们没法缩成小图塞进点名网格,单独成条、整幅展示。
      也别想着 .scale() 缩它:manim-web 的 scale 无条件量包围盒(显式 about_point 也拦不住),
      无 WebGL 的回归环境直接炸 getBounds。
"""

from manim import *


class PlanesScene(Scene):
    def construct(self):
        plane = ComplexPlane(x_range=[-4.2, 4.2, 1], y_range=[-2.4, 2.4, 1])
        # e^{iπ/3} = cos60° + i·sin60°,取半径 1.5 画在单位方向上
        marker = Dot([1.5 * 0.5, 1.5 * 0.866, 0], radius=0.08, color=YELLOW)
        spoke = Line([0, 0, 0], [1.5 * 0.5, 1.5 * 0.866, 0], color=YELLOW, stroke_width=2.5)
        label = MathTex(r"e^{i\pi/3}", font_size=34, color=YELLOW).move_to([1.9, 1.6, 0])

        self.play(Create(plane), run_time=1.0)
        self.play(Create(spoke), FadeIn(marker), Write(label), run_time=0.7)
        self.wait(0.4)
        self.play(FadeOut(plane), FadeOut(spoke), FadeOut(marker), FadeOut(label), run_time=0.8)

        polar = PolarPlane(size=6.2)
        marker2 = Dot([1.5 * 0.5, 1.5 * 0.866, 0], radius=0.08, color=YELLOW)
        spoke2 = Line([0, 0, 0], [1.5 * 0.5, 1.5 * 0.866, 0], color=YELLOW, stroke_width=2.5)
        label2 = Text("同一个方向:60°", font_size=26, color=YELLOW).move_to([2.7, 2.2, 0])

        self.play(Create(polar), run_time=1.0)
        self.play(Create(spoke2), FadeIn(marker2), Write(label2), run_time=0.7)
        self.wait(0.4)

MIT licensed · Manim 与 Pyodide 各依其自身许可(见「安装」)