Skip to content

From Python

The film's code
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()

A scene renders from Python as from the command line: MyScene().render() runs it and records its film. The film keeps the frames, the plays, the sounds, the sections and the captions, to write as a video or to read in a program. A window plays a film on the screen as it is recorded.

render

Run the scene and record its film: setup, construct and tear_down, then a closing frame.

Each frame is sent as it is made: encoded into video, an MP4 file, and handed to frames, if given; each play or wait, as it ends, is handed to plays, if given. A frame is drawn only for the video, or when frames asks for its pixels: with neither, the frames are counted, not drawn, which checks a scene quickly. Frames k = 0, 1, … show the world at k / fps seconds, before the scene's end; the closing frame then shows it at its own instant: the scene's end if a frame falls there, or else the first frame time after it. So the scene's last animation is seen landing.

Either function, and a take, may raise Cut to end the film there: the scene stops, and the video is written up to the last frame sent. If the scene fails, the video is not written. A scene renders once: make a new one to render it again.

self.render(video=None, *, preset='ultrafast', crf=18.0, frames=None, plays=None, take=None)
video

The MP4 file to write, if any, at the size and frame rate of config (a whole number of frames a second, and an even width and height).

preset

The x264 encoding preset; faster presets make larger files.

crf

The x264 constant rate factor, from 0 to 51; lower values give higher quality.

frames

A function handed each frame as it is sent (a FrameSink): a Frame, which knows its place in the film and draws its pixels when asked.

plays

A function called as each play or wait ends (a PlayHook), with the Play and the animations it played, the world as the play left it.

take

Record the film as a take instead, for manimgx's player to draw: a function handed its bytes as they are recorded (a Take). A take has no video, and no frames for frames.

Returns how many frames it has, its plays, sections, sounds and captions, and how its video was made.

Source

src/manimgx/scene.py

def render(
    self,
    video: str | os.PathLike[str] | None = None,
    *,
    preset: X264Preset = "ultrafast",
    crf: float = 18.0,
    frames: FrameSink | None = None,
    plays: PlayHook | None = None,
    take: Take | None = None,
) -> Film:
    """Run the scene and record its film: `setup`, `construct` and `tear_down`, then
    a closing frame.

    Each frame is sent as it is made: encoded into `video`, an MP4 file, and handed to
    `frames`, if given; each play or wait, as it ends, is handed to `plays`, if given.
    A frame is drawn only for the video, or when `frames` asks for its pixels: with
    neither, the frames are counted, not drawn, which checks a scene quickly. Frames
    `k = 0, 1, …` show the world at `k / fps` seconds, before the scene's end; the
    closing frame then shows it at its own instant: the scene's end if a frame falls
    there, or else the first frame time after it. So the scene's last animation is
    seen landing.

    Either function, and a take, may raise [`Cut`][manimgx.rendering.film.Cut] to end the
    film there: the scene stops, and the video is written up to the last frame sent.
    If the scene fails, the video is not written. A scene renders once: make a new
    one to render it again.

    Args:
        video: The MP4 file to write, if any, at the size and frame rate of
            [`config`][manimgx.config.config] (a whole number of frames a second,
            and an even width and height).
        preset: The x264 encoding preset; faster presets make larger files.
        crf: The x264 constant rate factor, from 0 to 51; lower values give
            higher quality.
        frames: A function handed each frame as it is sent (a
            [`FrameSink`][manimgx.rendering.film.FrameSink]): a [`Frame`][manimgx.rendering.film.Frame],
            which knows its place in the film and draws its pixels when asked.
        plays: A function called as each play or wait ends (a
            [`PlayHook`][manimgx.rendering.film.PlayHook]), with the [`Play`][manimgx.rendering.film.Play]
            and the animations it played, the world as the play left it.
        take: Record the film as a take instead, for manimgx's player to draw: a
            function handed its bytes as they are recorded (a
            [`Take`][manimgx.rendering.film.Take]). A take has no video, and no frames for
            `frames`.

    Returns:
        The film: how many frames it has, its plays, sections, sounds and captions,
        and how its video was made.
    """
    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 (
            closing > 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

A scene's film: its frames, as they are sent, its plays, and its sounds, sections and captions.

Scene.render makes one and returns it. Each frame is sent once: encoded into the video, if there is one, and handed to frames, if given; a frame equal to the one before is not sent again, but lengthens it. Each play, as it ends, is kept, and handed to plays, if given. The sounds the scene placed are mixed into the video's sound track as it is written.

A film can instead be recorded as a take: the engine's work written down — each shape once, then each frame's view and records, and at the end its sound and captions — for manimgx's player to play, at any moment: in a Window, live as the scene runs (manimgx preview), or in the browser, where Python has no GPU (Pyodide).

m.Film(video=None, *, frames=None, plays=None, take=None, preset='ultrafast', crf=18.0)
video

The MP4 file the frames are encoded into as they come, if any.

frames

A function handed each frame, a Frame (see FrameSink).

plays

A function called as each play ends (see PlayHook).

take

A function handed the take's bytes as they are recorded, frame by frame, if the film is recorded as a take (see Take).

preset

The x264 preset the video is encoded with: faster presets make larger files.

crf

The video's quality, as x264's constant rate factor, from 0 to 51: lower is better, the default value is 18.

Source

src/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 = "ultrafast",
    crf: float = 18.0,
) -> 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 = Fraction(fps).limit_denominator(1000)
    """How many frames a second the film has: the configuration's when it began."""
    # 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)
    """The path of the video being written; None once it is written, and if there is
    none."""
    self.frames = frames
    """The function each drawn frame is handed to, if any."""
    self.frame_count = 0
    """How many frames the film has so far, a hold counting every frame it lasts."""
    self.plays: list[Play] = []
    """The plays and waits the scene has run, in order: each a
    [`Play`][manimgx.rendering.film.Play]."""
    self.sections: list[Section] = [
        Section("unnamed", Fraction(0), 0, "default.normal", "")
    ]
    """The film's sections, in order: each a [`Section`][manimgx.rendering.film.Section],
    lasting until the next begins. The first begins with the film."""
    self.subcaptions: list[Caption] = []
    """The captions the scene added (see
    [`add_subcaption`][manimgx.Scene.add_subcaption]); [`captions`]
    [manimgx.Film.captions] adds those of its speech."""
    self.clips: list[Clip] = []
    """The sounds the scene placed, in the order it placed them: each a
    [`Clip`][manimgx.audio.sound.Clip], from its start in scene time."""
    self.export: Export | None = None
    """How the video was made, once it is written: an
    [`Export`][manimgx.rendering.film.Export]; None until then, and with no video."""
    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

How many frames a second the film has: the configuration's when it began.

video

The path of the video being written; None once it is written, and if there is none.

frames

The function each drawn frame is handed to, if any.

frame_count

How many frames the film has so far, a hold counting every frame it lasts.

plays

The plays and waits the scene has run, in order: each a Play.

sections

The film's sections, in order: each a Section, lasting until the next begins. The first begins with the film.

subcaptions

The captions the scene added (see add_subcaption); captions adds those of its speech.

clips

The sounds the scene placed, in the order it placed them: each a Clip, from its start in scene time.

export

How the video was made, once it is written: an Export; None until then, and with no video.

soundtrack

The film's sound: its clips mixed over its length, at RATE samples a second, one column per channel; None if the scene placed no sound.

film.soundtrack()
Source

src/manimgx/rendering/film.py

def soundtrack(self) -> "np.ndarray | None":
    """The film's sound: its clips mixed over its length, at
    [`RATE`][manimgx.audio.sound.RATE] samples a second, one column per channel; None if
    the scene placed no sound."""
    from manimgx.audio.sound import mix

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

cut

The sounds the film's end cuts short: each clip, with the seconds it loses.

film.cut()
Source

src/manimgx/rendering/film.py

def cut(self) -> list[tuple["Clip", float]]:
    """The sounds the film's end cuts short: each clip, with the seconds it loses."""
    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

The film's captions as subtitles, in SubRip (.srt) form: what players, editors and video sites read beside a video.

film.subtitles()
Source

src/manimgx/rendering/film.py

def subtitles(self) -> str:
    """The film's [`captions`][manimgx.Film.captions] as subtitles, in SubRip (`.srt`)
    form: what players, editors and video sites read beside a video."""

    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

The film's captions, in order of their start: those the scene added, and its speech's, a line of a few words at a time as they are said.

film.captions()
Source

src/manimgx/rendering/film.py

def captions(self) -> list[Caption]:
    """The film's captions, in order of their start: those the scene added, and its
    speech's, a line of a few words at a time as they are said."""
    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's player in a window on this machine's screen, which plays the takes it is sent.

A window is a Take: hand it to Scene.render as take, and it plays the film as it is recorded. Once it is closed, a film sent to it is cut (see Cut).

Its viewer can ask for another of the file's scenes (N and P).

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

The window's title.

time

Where its playhead starts, in seconds.

Source

src/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
    """Why the window failed, if it did (no screen to open on, no GPU): it is closed."""
    self._listening = threading.Thread(target=self._listen, daemon=True)
    self._listening.start()

failure

Why the window failed, if it did (no screen to open on, no GPU): it is closed.

open

Whether the window is open: until its viewer closes it, or it is closed.

window.open
Source

src/manimgx/rendering/window.py

def open(self) -> bool:
    """Whether the window is open: until its viewer closes it, or it is closed."""
    return self._process.poll() is None

wait

Wait until the window is closed, at most timeout seconds; whether it was.

window.wait(timeout=None)
Source

src/manimgx/rendering/window.py

def wait(self, timeout: float | None = None) -> bool:
    """Wait until the window is closed, at most `timeout` seconds; whether it was."""
    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

Close the window.

window.close()
Source

src/manimgx/rendering/window.py

def close(self) -> None:
    """Close the window."""
    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()

What a film holds

Frame

A frame of a film, as the film sends it: shown as frames index to index + repeat - 1 of the video.

A frame the world holds still for is sent once, shown repeat times. Its pixels are drawn only when asked for, by pixels.

index

Where it is in the film: the number of the first frame it is shown as.

repeat

How many frames it is shown for.

draw

The function that draws its pixels.

key

Whether it begins a section of the film.

Source

src/manimgx/rendering/film.py

def __init__(
    self, index: int, repeat: int, draw: Callable[[], bytes], key: bool = False
) -> None:
    self.index = index
    """Where it is in the film: the number of the first frame it is shown as, from
    0."""
    self.repeat = repeat
    """How many frames it is shown for: more than 1 for a hold."""
    self.key = key
    """Whether it begins a [section][manimgx.rendering.film.Section] of the film: the picture
    the section before it ends on."""
    self._draw = draw

index

Where it is in the film: the number of the first frame it is shown as, from 0.

repeat

How many frames it is shown for: more than 1 for a hold.

key

Whether it begins a section of the film: the picture the section before it ends on.

pixels

Draw the frame, now.

Drawing is what a frame costs: ask only for the frames you keep, and before the film sends its next frame.

frame.pixels()

Returns RGBA, a byte a channel, row after row from the top.

Source

src/manimgx/rendering/film.py

def pixels(self) -> bytes:
    """Draw the frame, now.

    Drawing is what a frame costs: ask only for the frames you keep, and before the
    film sends its next frame.

    Returns:
        Its pixels: RGBA, a byte a channel, row after row from the top.
    """
    return self._draw()

Play

One play, or wait, of a scene, as its film keeps it: its number, when it began and ended, and where the scene's code played it.

index

Its number, from 0: plays and waits are counted together.

start

When it began, in seconds of scene time, exactly.

end

When it ended, in seconds of scene time, exactly.

where

Where the scene's code played it: the file and line of the innermost call outside manimgx; None if there is none.

Section

A section of a film, as next_section begins one: it lasts until the next section begins, or the film ends.

name

Its name.

start

When it begins, in seconds of scene time: a frame's time, exactly.

frame

Its first frame's number: a keyframe of the video, where a player can start.

type

How a presentation plays it.

notes

What the presenter reads during it.

Export

How a film's video was made: what Film.export holds once the video is written.

seconds

How long the video took, in seconds: from its start, as the scene began rendering, to its file written.

x264

Of those, the seconds spent in x264, the H.264 encoder (on a thread of its own).

converted

The share of the video's 16 × 16 pixel blocks converted for the encoder, from 0 to 1: the others had not changed since the frame before, and were skipped.

bytes

The size of the file, in bytes.

Cut

An early end to a film: raised by a frame sink, a play hook or a take, it stops the scene there.

The film closes as it is: its video, if any, is written up to the last frame sent.

Take

Take = Callable[[bytes], object]

A function handed a film's take as it is recorded (see Film): bytes of the stream manimgx's player reads — uploads, frames and notes, then the film's sound and captions — in order, frame by frame; what it returns is ignored. It may raise Cut to end the film there: it is then sent nothing more (a Window closed by its viewer does).

FrameSink

FrameSink = Callable[[Frame], object]

A function handed each frame of a film as the film sends it, a Frame (see Scene.render); what it returns is ignored. It may raise Cut to end the film there.

PlayHook

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

A function called as each play or wait of a film ends (see Scene.render), with the Play and the animations it played, the world as the play left it; what it returns is ignored. It may raise Cut to end the film there.

manimgx.audio.sound.RATE

The soundtrack's sample rate: 48,000 samples a second, as video's sound has.