跳转至

公式生成地貌

平坦网格升起为蛋盒、 马鞍和钟形。每个曲面由其公式计算,随相机转动 弯曲为下一个。

examples/surface_plots.py
"""二元函数的图像,一个变为下一个。

z = f(x, y) 的图像是平面上方的地貌:在每个点 (x, y) 上方,高度为 f(x, y)。sin x cos y 是
峰与谷组成的蛋盒状曲面。(x² − y²)/4 是一个鞍面:沿一条轴向上弯曲,沿另一条轴向下弯曲,
因此其中心是平坦的,既非峰也非谷。2e^{−(x² + y²)/2} 是一个单独的钟形曲面,即二维正态分布
的形状。每个曲面由其公式绘制,且每一个都弯向下一个。
"""

from collections.abc import Callable

import numpy as np

import manimgx as m

WALL = 4.2  # README 中影片墙的 5 秒从此开始
RESOLUTION = 48
COLORS = [
    (m.BLUE_E, -2.0),
    (m.BLUE, -1.0),
    (m.TEAL, 0.0),
    (m.GREEN, 1.0),
    (m.YELLOW, 2.0),
]

FUNCTIONS: list[tuple[str, Callable[[float, float], float]]] = [
    (r"z = \sin x \cos y", lambda x, y: np.sin(x) * np.cos(y)),
    (r"z = \frac{x^2 - y^2}{4}", lambda x, y: (x * x - y * y) / 4),
    (r"z = 2e^{-(x^2 + y^2)/2}", lambda x, y: 2 * np.exp(-(x * x + y * y) / 2)),
]


class SurfacePlots(m.ThreeDScene):
    def construct(self) -> None:
        axes = m.ThreeDAxes(
            x_range=[-3, 3, 1],
            y_range=[-3, 3, 1],
            z_range=[-2, 2, 1],
            x_length=6.6,
            y_length=6.6,
            z_length=4.4,
        )

        def surface(f: Callable[[float, float], float]) -> m.Surface:
            graph = m.Surface(
                lambda u, v: axes.c2p(u, v, f(u, v)),
                u_range=[-3, 3],
                v_range=[-3, 3],
                resolution=(RESOLUTION, RESOLUTION),
                fill_opacity=0.95,
                stroke_color=m.WHITE,
                stroke_width=0.4,
                stroke_opacity=0.35,
            )
            graph.set_fill_by_value(axes=axes, colorscale=COLORS, axis=2)
            return graph

        def label(tex: str) -> m.MathTex:
            formula = m.MathTex(tex, font_size=60).to_corner(m.UL, buff=0.5)
            self.add_fixed_in_frame_mobjects(formula)
            self.remove(formula)
            return formula

        self.set_camera_orientation(
            phi=64 * m.DEGREES, theta=-55 * m.DEGREES, zoom=0.95
        )
        self.begin_ambient_camera_rotation(rate=0.16)
        graph = surface(lambda x, y: 0.0)
        heights = [graph.points.copy()] + [
            surface(f).points.copy() for _, f in FUNCTIONS
        ]
        labels = [label(tex) for tex, _ in FUNCTIONS]

        def bend(k: int) -> m.Animation:
            """从第 k 个曲面到下一个(第 0 个是平面),在过渡过程中按高度重新着色。"""

            def step(mob: m.Mobject, alpha: float) -> None:
                assert isinstance(mob, m.Surface)
                mob.points = (1 - alpha) * heights[k] + alpha * heights[k + 1]
                mob.set_fill_by_value(axes=axes, colorscale=COLORS, axis=2)

            return m.UpdateFromAlphaFunc(graph, step)

        self.play(m.Create(axes), run_time=1.2)
        self.play(m.FadeIn(graph), m.Write(labels[0]), run_time=0.8)
        self.play(bend(0), run_time=2)
        self.wait(1)
        for k in (1, 2):
            self.play(
                bend(k),
                m.FadeOut(labels[k - 1], shift=0.4 * m.UP),
                m.FadeIn(labels[k], shift=0.4 * m.UP),
                run_time=2.6,
            )
            self.wait(1.4)
        self.wait(2)


if __name__ == "__main__":
    SurfacePlots().render("surface_plots.mp4")