跳转至

从 Python

影片代码
import manimgx as m


class FromPythonHero(m.Scene):
    def construct(self) -> None:
        code = m.Code(
            code_string='film = MyScene().render()\nfilm.export("video.mp4")\nprint(film.subtitles())',
            language="python",
            background="window",
        ).scale(1.1)
        self.play(m.FadeIn(code))
        self.wait()

场景从 Python 渲染如同从命令行渲染:MyScene().render() 运行它并记录它的影片。影片保留帧、play、声音、段落和字幕,以写入为视频或在程序中读取。窗口在录制时在屏幕上播放影片。

render

运行场景并记录其影片:setup、construct 与 tear_down,然后 一个闭合帧。

每一帧在生成时即被发送:编码进 video(一个 MP4 文件),并交给 frames(若给定);每次 play 或 wait 结束时,交给 plays(若给定)。 仅在需要视频或当 frames 请求其像素时才绘制帧:二者皆无时,帧仅被 计数而不绘制,可快速检查场景。帧 k = 0, 1, … 在同一浮点时刻的事件 之后采样 k / fps 秒;闭合帧随后在其自身时刻显示场景:若某帧恰落在 场景结束处则为该结束时刻,否则为之后的第一个帧时刻。因此场景的最后一个 动画的落定被看见。

任一函数以及一个 take 都可抛出 Cut 以在 该处结束影片:场景停止,视频写到已发送的最后一帧。若场景失败,视频不 被写入。一个场景只渲染一次:再次渲染需新建一个。

self.render(video=None, *, preset=DEFAULT_ENCODER_PRESET, crf=DEFAULT_ENCODER_CRF, frames=None, plays=None, take=None)
video

要写入的 MP4 文件(若有),尺寸与帧率取 config(每秒整帧数,且宽高均为偶数)。

preset

x264 编码预设;更快的预设产生更大的文件。

crf

x264 的恒定速率因子,从 0 到 51;值越低质量越高。

frames

一个随每帧发送而被调用的函数(一个 FrameSink):一个 Frame,它知道自己在影片中的位置, 并在被请求时绘制其像素。

plays

一个在每次 play 或 wait 结束时被调用的函数(一个 PlayHook),带有 Play 及其播放的动画,以及 play 留下的世界。

take

改为将影片记录为一个 take,供 ManimGX 的播放器绘制:一个随其 字节被记录而调用的函数(一个 Take)。take 没有视频,也没有供 frames 的帧。

返回 影片:其帧数、play、分段、声音与字幕,以及其视频的生成方式。

源码

manimgx\scene.py

def render(
    self,
    video: str | os.PathLike[str] | None = None,
    *,
    preset: X264Preset = DEFAULT_ENCODER_PRESET,
    crf: float = DEFAULT_ENCODER_CRF,
    frames: FrameSink | None = None,
    plays: PlayHook | None = None,
    take: Take | None = None,
) -> Film:
    """运行场景并记录其影片:`setup`、`construct` 与 `tear_down`,然后
    一个闭合帧。

    每一帧在生成时即被发送:编码进 `video`(一个 MP4 文件),并交给
    `frames`(若给定);每次 play 或 wait 结束时,交给 `plays`(若给定)。
    仅在需要视频或当 `frames` 请求其像素时才绘制帧:二者皆无时,帧仅被
    计数而不绘制,可快速检查场景。帧 `k = 0, 1, …` 在同一浮点时刻的事件
    之后采样 `k / fps` 秒;闭合帧随后在其自身时刻显示场景:若某帧恰落在
    场景结束处则为该结束时刻,否则为之后的第一个帧时刻。因此场景的最后一个
    动画的落定被看见。

    任一函数以及一个 take 都可抛出 [`Cut`][manimgx.rendering.film.Cut] 以在
    该处结束影片:场景停止,视频写到已发送的最后一帧。若场景失败,视频不
    被写入。一个场景只渲染一次:再次渲染需新建一个。

    Args:
        video: 要写入的 MP4 文件(若有),尺寸与帧率取
            [`config`][manimgx.config.config](每秒整帧数,且宽高均为偶数)。
        preset: x264 编码预设;更快的预设产生更大的文件。
        crf: x264 的恒定速率因子,从 0 到 51;值越低质量越高。
        frames: 一个随每帧发送而被调用的函数(一个
            [`FrameSink`][manimgx.rendering.film.FrameSink]):一个
            [`Frame`][manimgx.rendering.film.Frame],它知道自己在影片中的位置,
            并在被请求时绘制其像素。
        plays: 一个在每次 play 或 wait 结束时被调用的函数(一个
            [`PlayHook`][manimgx.rendering.film.PlayHook]),带有
            [`Play`][manimgx.rendering.film.Play] 及其播放的动画,以及 play
            留下的世界。
        take: 改为将影片记录为一个 take,供 ManimGX 的播放器绘制:一个随其
            字节被记录而调用的函数(一个
            [`Take`][manimgx.rendering.film.Take])。take 没有视频,也没有供
            `frames` 的帧。

    Returns:
        影片:其帧数、play、分段、声音与字幕,以及其视频的生成方式。
    """
    self.film = Film(
        video, frames=frames, plays=plays, take=take, preset=preset, crf=crf
    )
    clock.reset()
    try:
        self.setup()
        self.construct()
        self.tear_down()
        closing = Fraction(self.frame) / self._fps()
        if float(closing) > float(
            clock.now
        ):  # the scene ended between frames: the world at frame N
            self._records = self._recording()
            self._frame(closing, self._simulation_rate())
        self._emit()
    except Cut:
        pass  # the film ends where it was cut
    except BaseException:
        self.film.abort()
        raise
    self.film.close()
    return self.film

Film

一个场景的影片:它的帧(按发出顺序)、它的播放,以及它的声音、分段 和字幕。

Scene.render 创建并返回一个影片。每一帧只发出一次: 若有视频则编码入视频,若提供了 frames 则交予它;与前一帧相同的帧不会再次 发出,而是延长前者。每一次播放在结束时被保留,若提供了 plays 则交予它。 场景放置的声音在视频写入时混入其音轨。

影片也可改为作为 take 录制:将引擎的工作写下——每个形状一次,随后每帧的 视图与记录,最后是声音与字幕——供 ManimGX 的播放器随时播放:在 Window 中、场景运行时实时播放(manimgx preview),或在 浏览器中(Python 无 GPU,Pyodide 环境)。

m.Film(video=None, *, frames=None, plays=None, take=None, preset=DEFAULT_ENCODER_PRESET, crf=DEFAULT_ENCODER_CRF)
video

帧到来时编码入的 MP4 文件,若有。

frames

一个被交给每一帧的函数,参数为一个 Frame (见 FrameSink)。

plays

一个在每次播放结束时被调用的函数(见 PlayHook)。

take

一个在影片作为 take 录制时被逐帧交给 take 字节的函数,若有 (见 Take)。

preset

视频编码所用的 x264 预设:越快的预设产生越大的文件。

crf

视频的质量,以 x264 的恒定速率因子表示,从 0 到 51: 越低越好,默认值为 18。

源码

manimgx\rendering\film.py

def __init__(
    self,
    video: str | os.PathLike[str] | None = None,
    *,
    frames: FrameSink | None = None,
    plays: PlayHook | None = None,
    take: Take | None = None,
    preset: X264Preset = DEFAULT_ENCODER_PRESET,
    crf: float = DEFAULT_ENCODER_CRF,
) -> None:
    if preset not in get_args(X264Preset):
        raise ValueError(f"unknown x264 preset {preset!r}")
    if not 0 <= crf <= 51:
        raise ValueError(
            f"x264 CRF must be a finite number from 0 to 51, not {crf}"
        )
    fps = config.frame_rate
    if video is not None and fps != int(fps):
        raise ValueError(
            f"a video needs a whole number of frames per second, not {fps}"
        )
    width, height = config.pixel_width, config.pixel_height
    if video is not None and (width % 2 or height % 2):  # H.264's 4:2:0 halves both
        raise ValueError(
            f"a video needs an even width and height, not {width}x{height}"
        )
    self.fps = clock.rational(fps)
    """影片每秒的帧数:影片开始时配置所定的值。"""
    # the GPU draws the film; or its take is recorded, to be drawn by ManimGX's player: by
    # choice, or where the engine has no GPU (in Pyodide)
    self._player: _engine.Player | None = None
    self._recorder: _engine.Recorder | None = None
    drawn: _engine.Player | _engine.Recorder
    if take is None and (gpu := feed.Player) is not None:
        if video is not None or frames is not None:
            _engine.start_gpu()  # the film draws: the GPU comes up as it begins
        self._player = drawn = gpu(width, height)
        if video is not None:
            drawn.begin_export(
                os.fspath(video), fps=int(fps), preset=preset, crf=crf
            )
    elif video is None and frames is None:
        self._recorder = drawn = _engine.Recorder(width, height, fps)
    else:
        raise ValueError(
            "a film recorded as a take is drawn by ManimGX's player, not here: it"
            " has no video or pixels"
            + ("" if feed.Player else " (this Python has no GPU)")
        )
    self._take = take
    self.feeder = Feeder(width, height, drawn)
    self.video = None if video is None else os.fspath(video)
    """正在写入的视频路径;写入完成后为 None,若无视频
    则始终为 None。"""
    self.frames = frames
    """每个已绘制的帧被交予的函数,若有。"""
    self.frame_count = 0
    """影片迄今为止的帧数,保持帧按其持续的帧数计入。"""
    self.plays: list[Play] = []
    """场景已运行的播放与等待,按顺序排列:每个为一个
    [`Play`][manimgx.rendering.film.Play]。"""
    self.sections: list[Section] = [
        Section("unnamed", Fraction(0), 0, "default.normal", "")
    ]
    """影片的分段,按顺序排列:每个为一个 [`Section`][manimgx.rendering.film.Section],
    持续到下一分段开始。第一个分段随影片开始。"""
    self.subcaptions: list[Caption] = []
    """场景添加的字幕(见
    [`add_subcaption`][manimgx.Scene.add_subcaption]);[`captions`]
    [manimgx.Film.captions] 会并入其语音产生的字幕。"""
    self.clips: list[Clip] = []
    """场景放置的声音,按放置顺序排列:每个为一个
    [`Clip`][manimgx.audio.sound.Clip],自其在场景时间中的起点计。"""
    self.export: Export | None = None
    """视频的制作情况,视频写入完成后为一个
    [`Export`][manimgx.rendering.film.Export];在此之前以及无视频时为 None。"""
    self._hook = plays
    self._pending: tuple[bytes, bytes, list[CameraView], int] | None = (
        None  # sent once it ends
    )
    self._first = 0  # the index of the pending frame
    self._key = False  # the next frame begins a section: a keyframe of its own
    self._pending_key = False  # the pending frame is one

fps

影片每秒的帧数:影片开始时配置所定的值。

video

正在写入的视频路径;写入完成后为 None,若无视频 则始终为 None。

frames

每个已绘制的帧被交予的函数,若有。

frame_count

影片迄今为止的帧数,保持帧按其持续的帧数计入。

plays

场景已运行的播放与等待,按顺序排列:每个为一个 Play。

sections

影片的分段,按顺序排列:每个为一个 Section, 持续到下一分段开始。第一个分段随影片开始。

subcaptions

场景添加的字幕(见 add_subcaption);captions 会并入其语音产生的字幕。

clips

场景放置的声音,按放置顺序排列:每个为一个 Clip,自其在场景时间中的起点计。

export

视频的制作情况,视频写入完成后为一个 Export;在此之前以及无视频时为 None。

soundtrack

影片的声音:其各片段在其时长上混音的结果,采样率为 RATE 每秒采样数,每通道一列;若场景 未放置任何声音则为 None。

film.soundtrack()
源码

manimgx\rendering\film.py

def soundtrack(self) -> "np.ndarray | None":
    """影片的声音:其各片段在其时长上混音的结果,采样率为
    [`RATE`][manimgx.audio.sound.RATE] 每秒采样数,每通道一列;若场景
    未放置任何声音则为 None。"""
    from manimgx.audio.sound import mix

    return mix(self.clips, self.frame_count / self.fps)

cut

影片结尾所截短的声音:每个片段及其损失的秒数。

film.cut()
源码

manimgx\rendering\film.py

def cut(self) -> list[tuple["Clip", float]]:
    """影片结尾所截短的声音:每个片段及其损失的秒数。"""
    end = self.frame_count / self.fps
    out = []
    for clip in self.clips:
        if clip.end is None and clip.sound.duration != float("inf"):
            lost = float(clip.start) + clip.sound.duration - float(end)
            if lost > 1e-3:
                out.append((clip, lost))
    return out

subtitles

影片的 captions 以 SubRip(.srt) 格式表示的字幕:播放器、编辑器与视频站点在视频旁所读的内容。

film.subtitles()
源码

manimgx\rendering\film.py

def subtitles(self) -> str:
    """影片的 [`captions`][manimgx.Film.captions] 以 SubRip(`.srt`)
    格式表示的字幕:播放器、编辑器与视频站点在视频旁所读的内容。"""

    def stamp(t: float) -> str:
        ms = round(t * 1000)
        return f"{ms // 3_600_000:02d}:{ms // 60_000 % 60:02d}:{ms // 1000 % 60:02d},{ms % 1000:03d}"

    return "".join(
        f"{i}\n{stamp(c.start)} --> {stamp(c.end)}\n{c.text}\n\n"
        for i, c in enumerate(self.captions(), 1)
    )

captions

影片的字幕,按其开始时刻排序:场景添加的那些,以及其语音 产生的那些——按所说时刻分成每行数词一行。

film.captions()
源码

manimgx\rendering\film.py

def captions(self) -> list[Caption]:
    """影片的字幕,按其开始时刻排序:场景添加的那些,以及其语音
    产生的那些——按所说时刻分成每行数词一行。"""
    from manimgx.audio import Speech, lines

    spoken = [
        Caption(float(clip.start) + a, float(clip.start) + b, text)
        for clip in self.clips
        if isinstance(clip.sound, Speech)
        for a, b, text in lines(clip.sound)
    ]
    return sorted(self.subcaptions + spoken, key=lambda c: c.start)

Window

ManimGX 的播放器,运行在本机屏幕的窗口中,播放发送给它的片段。

窗口是一个 Take:将其传给 Scene.render 作为 take,它会在录制时播放影片。 一旦窗口关闭,发送给它的影片即被剪切(见 Cut)。

其查看者可请求该文件的其它场景(N 和 P)。

m.Window(title='ManimGX', *, time=0.0)
title

窗口的标题。

time

播放头起始位置,以秒为单位。

源码

manimgx\rendering\window.py

def __init__(self, title: str = "ManimGX", *, time: float = 0.0) -> None:
    self._process = subprocess.Popen(
        [
            sys.executable,
            "-c",
            "from manimgx.rendering.window import _main; _main()",
        ]
        + [title, repr(float(time))],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
    )
    self._asked: queue.Queue[str] = queue.Queue()
    self.failure: str | None = None
    """窗口失败的原因(若失败):无可打开的屏幕、无 GPU 等:此时窗口已关闭。"""
    self._listening = threading.Thread(target=self._listen, daemon=True)
    self._listening.start()

failure

窗口失败的原因(若失败):无可打开的屏幕、无 GPU 等:此时窗口已关闭。

open

窗口是否处于打开状态:直至其查看者关闭它,或其自身被关闭。

window.open
源码

manimgx\rendering\window.py

def open(self) -> bool:
    """窗口是否处于打开状态:直至其查看者关闭它,或其自身被关闭。"""
    return self._process.poll() is None

wait

等待直至窗口关闭,最多等待 timeout 秒;返回是否已关闭。

window.wait(timeout=None)
源码

manimgx\rendering\window.py

def wait(self, timeout: float | None = None) -> bool:
    """等待直至窗口关闭,最多等待 `timeout` 秒;返回是否已关闭。"""
    try:
        self._process.wait(timeout)
    except subprocess.TimeoutExpired:
        return False
    self._listening.join()  # what it said before it closed, `failure` among it
    return True

close

关闭窗口。

window.close()
源码

manimgx\rendering\window.py

def close(self) -> None:
    """关闭窗口。"""
    pipe = self._process.stdin
    if pipe is not None and not pipe.closed:
        with contextlib.suppress(OSError):
            pipe.close()  # its end of the take: it closes
    if not self.wait(10):
        self._process.kill()
        self.wait()

影片所包含的内容

Frame

影片的一帧,由影片发出时所示:显示为视频的第 index 到 index + repeat - 1 帧。

世界保持静止的一帧只发出一次,显示 repeat 次。它的像素仅在 被请求时才绘制,由 pixels 完成。

index

它在影片中的位置:它作为首帧显示时的帧号。

repeat

它显示的帧数。

draw

绘制其像素的函数。

key

它是否开启影片的一个分段。

源码

manimgx\rendering\film.py

def __init__(
    self, index: int, repeat: int, draw: Callable[[], bytes], key: bool = False
) -> None:
    self.index = index
    """它在影片中的位置:它作为首帧显示时的帧号,从
    0 起计。"""
    self.repeat = repeat
    """它显示的帧数:大于 1 表示一次保持。"""
    self.key = key
    """它是否开启影片的一个 [分段][manimgx.rendering.film.Section]:其前一分段
    结束时所停留的画面。"""
    self._draw = draw

index

它在影片中的位置:它作为首帧显示时的帧号,从 0 起计。

repeat

它显示的帧数:大于 1 表示一次保持。

key

它是否开启影片的一个 分段:其前一分段 结束时所停留的画面。

pixels

立即绘制该帧。

绘制即一帧的代价:只为要保留的帧请求,且在影片发出下一帧之前。

frame.pixels()

返回 它的像素:RGBA,每通道一个字节,自顶向下逐行排列。

源码

manimgx\rendering\film.py

def pixels(self) -> bytes:
    """立即绘制该帧。

    绘制即一帧的代价:只为要保留的帧请求,且在影片发出下一帧之前。

    Returns:
        它的像素:RGBA,每通道一个字节,自顶向下逐行排列。
    """
    return self._draw()

Play

场景的一次播放或等待,由其影片所保留:它的编号、起止时刻, 以及场景代码播放它的位置。

index

它的编号,从 0 起计:播放与等待一并计数。

start

它开始的时刻,以场景时间的秒计,精确。

end

它结束的时刻,以场景时间的秒计,精确。

where

场景代码播放它的位置:manimgx 之外最内层调用的文件与行号; 若无则为 None。

Section

影片的一个分段,由 next_section 开启:它 持续到下一分段开启,或影片结束。

name

它的名字。

start

它开始的时刻,以场景时间的秒计:某一帧的时刻,精确。

frame

它首帧的编号:视频的一个关键帧,播放器可从此处开始。

type

演示如何播放它。

notes

演示者在此期间所读的内容。

Export

影片视频的制作情况:视频写入完成后 Film.export 所 持有的内容。

seconds

视频所耗时长,以秒计:自其开始(即场景开始渲染时)至 其文件写入完成。

x264

其中花在 x264(即 H.264 编码器,运行于其独立线程)上的 秒数。

converted

视频中为编码器转换的 16 × 16 像素块所占比例,从 0 到 1:其余的相对前一帧未发生变化,被跳过。

bytes

文件的大小,以字节计。

Cut

影片的提前结束:由帧接收器、播放钩子或 take 抛出,使场景 在此处停止。

影片按现状关闭:其视频(若有)写入至已发出的最后一帧。

Take

Take = Callable[[bytes], object]

一个函数,在影片作为 take 录制时被交给影片的 take 字节(见 Film): 即 manimgx 播放器所读取的流字节——上传信息、帧与备注,随后是影片的 声音与字幕——按序逐帧送达;其返回值被忽略。它可以抛出 Cut 以在此处结束影片:此后不再向其发送任何内容(由 其观看者关闭的 Window 即如此)。

FrameSink

FrameSink = Callable[[Frame], object]

一个函数,影片发出每一帧时将其交给它,参数为一个 Frame(见 Scene.render);其返回值 被忽略。它可以抛出 Cut 以在此处结束影片。

PlayHook

PlayHook = Callable[[Play, tuple[Animation, ...]], object]

一个函数,在影片的每次播放或等待结束时被调用(见 Scene.render),参数为该 Play 及 其播放的动画、播放结束时世界所处的状态;其返回值被忽略。它可以抛出 Cut 以在此处结束影片。

manimgx.audio.sound.RATE

音轨的采样率:每秒 48,000 个采样点,与视频的声音一致。