# ManimGL (3b1b/manim) Administrator and Developer Guide

This guide covers the internals of `3b1b/manim` (ManimGL): the rendering
pipeline, the OpenGL/shader stack, configuration layering, the package
layout on disk, the interactive shell internals, and what every module
contributes. It is intended for developers who want to extend ManimGL,
operators who want to run it at scale (classrooms, CI, video farms), and
advanced users who need to understand failure modes that the User Guide
doesn't reach.

> [!IMPORTANT]
> This is the original Grant Sanderson library, **not** ManimCE
> (`manim-community/manim`). The two share an ancestor but have diverged on
> module layout, class names, default configuration, the renderer
> implementation, and the CLI binary. Everything in this guide describes
> ManimGL only.

## 1. Architecture Overview

ManimGL is a Python animation framework with a hard dependency on OpenGL
3.3+ via `moderngl`. The high-level execution flow:

```
              +-------------------------+
              |  manimgl <file> <Scene> |
              +------------+------------+
                           |
                           v
              +-------------------------+
              |    manimlib.__main__    |
              |   parse_cli, run loop   |
              +------------+------------+
                           |
                           v
              +-------------------------+      +-----------------+
              |   manim_config (Dict)   |<-----+ default_config  |
              |  (yaml + cli + custom)  |      | custom_config   |
              +------------+------------+      +-----------------+
                           |
                           v
              +-------------------------+
              |   ModuleLoader.get      |   import the user's .py file
              |   _module(file)         |   (with optional reload tracking)
              +------------+------------+
                           |
                           v
              +-------------------------+
              | extract_scene.main      |   discover Scene classes
              |  -> get_scenes_to_run   |   instantiate them
              +------------+------------+
                           |
                           v
              +-------------------------+
              |  Scene.run() loop       |
              |  setup -> construct     |
              |  -> interact -> tear    |
              +-----+--------------+----+
                    |              |
                    v              v
        +------------------+    +-------------------+
        |     Camera       |    |  SceneFileWriter  |
        | OpenGL context,  |    |  ffmpeg subprocess|
        | FBOs, light src  |    |  partial movies,  |
        +--------+---------+    |  audio mux        |
                 |              +-------------------+
                 v
        +------------------+
        |  ShaderWrapper   |
        | / VShaderWrapper |
        | per-mob program  |
        +--------+---------+
                 |
                 v
        +------------------+
        |    moderngl      |
        |    OpenGL 3.3    |
        +------------------+
```

A scene is a Python class that, on `.run()`, calls `setup()`, then
`construct()`, then enters an idle event loop (`interact()`) if a window
is attached. Every `self.play(...)` invocation feeds a list of `Animation`
objects through `pre_play -> begin_animations -> progress_through_animations
-> finish_animations -> post_play`. Inside the inner loop, the scene
advances time by `1 / fps` seconds per iteration, calls each animation's
`interpolate(alpha)`, calls every mobject's updaters, and asks the camera
to capture a frame. If `write_to_movie=True`, that frame is piped to FFmpeg
via stdin.

> [!NOTE]
> ManimGL has **no message bus and no parallelism inside a scene**. A
> scene runs single-threaded. The only out-of-process work is the FFmpeg
> encoder subprocess (`SceneFileWriter.writing_process`) and the LaTeX
> compiler subprocess invoked by `Tex` / `TexText` mobjects.

### 1.1 Component Inventory

| Component | Path | Role |
|---|---|---|
| **Entry point** | `manimlib/__main__.py` | Parses CLI, builds config, runs the scene loop. |
| **Config** | `manimlib/config.py`, `manimlib/default_config.yml` | Argparse spec, YAML merge, global `manim_config`. |
| **Constants** | `manimlib/constants.py` | Frame size, vectors, colors, angles. |
| **Logger** | `manimlib/logger.py` | `rich`-backed logger named `"manimgl"`. |
| **Scene discovery** | `manimlib/extract_scene.py` | Imports the user file, finds `Scene` subclasses, prompts if needed. |
| **Module loader** | `manimlib/module_loader.py` | Import-with-tracking for hot reload. |
| **Window** | `manimlib/window.py` | Pyglet OpenGL window, event delegation to `Scene`. |
| **Camera** | `manimlib/camera/camera.py`, `camera_frame.py` | OpenGL context, FBOs, light source, `CameraFrame` mobject. |
| **Shaders** | `manimlib/shader_wrapper.py`, `manimlib/shaders/*.glsl` | `ShaderWrapper` / `VShaderWrapper`, GLSL programs. |
| **Mobject base** | `manimlib/mobject/mobject.py` | `Mobject`, `Group`, `Point`, animation/updater builders. |
| **Mobjects** | `manimlib/mobject/**/*.py` | All shape, text, 3D, matrix, vector-field classes. |
| **Animations** | `manimlib/animation/**/*.py` | `Animation` base + every concrete animation class. |
| **Scene** | `manimlib/scene/scene.py` | `Scene`, `SceneState`, `EndScene`, `ThreeDScene`. |
| **Interactive scene** | `manimlib/scene/interactive_scene.py` | Drag-select, palette, undo/redo. |
| **Embedded shell** | `manimlib/scene/scene_embed.py` | `InteractiveSceneEmbed`, `CheckpointManager` (IPython integration). |
| **File writer** | `manimlib/scene/scene_file_writer.py` | FFmpeg pipeline, partial movies, audio. |
| **Event handler** | `manimlib/event_handler/` | Singleton `EVENT_DISPATCHER`, `EventListener`, `EventType` enum. |
| **Utilities** | `manimlib/utils/*.py` | Bezier, color, file ops, rate functions, space ops, TeX writing, sounds. |
| **Type aliases** | `manimlib/typing.py` | `ManimColor`, `Vect3`, `Vect3Array`, etc. |
| **Examples** | `example_scenes.py`, `logo/logo.py` | Runnable demonstrations. |

### 1.2 Technology Stack

| Layer | Implementation |
|---|---|
| Language | Python 3.8+ (modern type hints, `from __future__ import annotations` ubiquitous) |
| Window/event loop | `moderngl_window` / `pyglet` |
| OpenGL bindings | `moderngl` 5.x + `PyOpenGL` |
| Shaders | GLSL 3.30 core profile (in `manimlib/shaders/*.glsl`) |
| Vector math | `numpy` 1.x; `scipy.linalg` for solvers |
| Geometry | `mapbox-earcut` (triangulation), `skia-pathops` (booleans), `isosurfaces` (implicit functions), `trimesh`, `pywavefront`, `fontTools.cu2qu` |
| LaTeX | External `latex` / `xelatex` binaries; output piped through `dvisvgm` to SVG; parsed by `svgelements` |
| Text shaping | `manimpango>=0.6.0` (Pango / HarfBuzz wrapper) |
| Color | `colour` package |
| Audio | `pydub` (loaded files); piped to FFmpeg for mux |
| Config | `pyyaml`, wrapped in `addict.Dict` for attribute access |
| Cache | `diskcache`, plus a custom SVG cache for compiled `Tex` / `Text` |
| CLI | Stdlib `argparse` |
| Logging | Stdlib `logging` + `rich.logging.RichHandler` |
| Interactive shell | `IPython.terminal.embed` |
| Subprocess | `subprocess.Popen` for FFmpeg |
| Clipboard | `pyperclip` for `InteractiveScene` copy/paste |
| Multi-monitor | `screeninfo` |

### 1.3 Data Flow for the Render Pipeline

For each `self.play(*animations)`:

1. **`pre_play`** — Update skipping status (jump-to-N, etc.), tell the
   `SceneFileWriter` to `begin_animation()` (creates a partial movie file
   if `subdivide_output` is on, opens a new FFmpeg pipe, and writes
   leading audio padding).
2. **`begin_animations`** — Call `animation.begin()` on each. Each
   `Animation` saves a `starting_mobject = self.mobject.copy()`, marks
   the mobject as animating, and zips together the families of source
   and target mobjects so per-submobject interpolation stays aligned.
3. **`progress_through_animations`** — Iterate `dt`-spaced timestamps
   from `0` to `run_time`. For each `t`:
   - Each animation's `update_mobjects(dt)` runs updaters on auxiliary
     copies (e.g. `target_mobject` for `Transform`).
   - Each animation's `interpolate(alpha)` runs (where
     `alpha = t / run_time`).
   - The scene advances time by `dt`, runs every mobject's updaters,
     and calls `camera.capture(*self.render_groups)` to draw.
   - If a window is attached, the renderer sleeps to keep wall-clock
     synchronized to virtual time.
   - If `write_to_movie`, the captured frame is piped to FFmpeg.
4. **`finish_animations`** — Each animation's `finish()` runs (sets
   `alpha=final_alpha_value`, default `1.0`), then `clean_up_from_scene`
   (removes the mobject if `is_remover()`).
5. **`post_play`** — Increment `num_plays`, end the partial movie file,
   and (in preview-while-skipping mode) draw one quick frame.

For `self.wait(duration, stop_condition=None)`:

- Same envelope (`pre_play`, then a time loop, then `post_play`), but the
  inner loop only updates mobjects/frames; no animation interpolation.
- If `presenter_mode` is on and not skipping, the loop blocks on
  `hold_loop()` until the user presses Space or right-arrow.

> [!NOTE]
> Inside an animation, frame timestamps are **virtual time**, not
> wall-clock. Wall-clock sleeping happens only when a window is attached
> to keep the preview real-time. When writing to file, ManimGL renders
> as fast as it can.

## 2. System Requirements

| Resource | Recommended |
|---|---|
| CPU | Modern x86_64 / ARM64. Single-threaded, so clock speed matters more than core count. |
| RAM | 4 GB minimum. Heavy 3D + LaTeX scenes can push 8 GB. |
| GPU | OpenGL 3.3+ core profile. Integrated GPUs work. Low VRAM (~256 MB) is fine for 1080p. |
| Disk | ~200 MB for the package + dependencies; rendered videos run anywhere from 5 MB/min (low quality) to 200 MB/min (4K, low-compression codecs). |
| Python | 3.8 or newer. Tested on CPython; PyPy is not supported. |
| External binaries | `ffmpeg` (mandatory for video output), `latex` + `dvisvgm` (only for `Tex`/`TexText`). |

> [!CAUTION]
> ManimGL allocates a multi-megapixel float framebuffer per fill draw
> (see `VShaderWrapper.get_fill_canvas` — it builds a texture at
> `(2 * resolution[0], 2 * resolution[1])` for fill compositing). At 4K
> with `samples=4`, this can exceed 1 GB of VRAM transiently. Reduce
> `camera.resolution` or `samples` if you hit GL_OUT_OF_MEMORY.

## 3. Configuration Reference

### 3.1 Configuration Files

| File | Format | Location | Purpose |
|---|---|---|---|
| `default_config.yml` | YAML | `manimlib/default_config.yml` (in package) | Hard-coded defaults shipped with ManimGL. |
| `custom_config.yml` | YAML | Current working directory at runtime | Per-project overrides. |
| `<custom>.yml` | YAML | Path passed to `--config_file` | Ad-hoc overrides. |
| `tex_templates.yml` | YAML | `manimlib/tex_templates.yml` | LaTeX preamble bundles indexed by name. |

`config.initialize_manim_config()` merges the three YAML sources via
`merge_dicts_recursively` (later sources override earlier ones), then
mutates the resulting `addict.Dict` based on parsed CLI args via
`update_directory_config`, `update_window_config`, `update_camera_config`,
`update_file_writer_config`, `update_scene_config`, `update_run_config`,
and `update_embed_config`. The final dict is exported as the module-level
`manim_config`.

### 3.2 Top-Level Schema

```yaml
directories:
  mirror_module_path: false
  base: ""
  subdirs:
    output: "videos"
    raster_images: "raster_images"
    vector_images: "vector_images"
    three_d_models: "three_d_models"
    sounds: "sounds"
    data: "data"
    downloads: "downloads"
    latex_cache: "latex_cache"
  cache: ""

window:
  position_string: UR
  monitor_index: 0
  full_screen: false
  # position: (500, 500)        # optional, pixel coords for upper-right corner
  # size: (1920, 1080)          # optional, exact pixel size

camera:
  resolution: (1920, 1080)
  background_color: "#333333"
  fps: 30
  background_opacity: 1.0

file_writer:
  ffmpeg_bin: "ffmpeg"
  video_codec: "libx264"
  pixel_format: "yuv420p"
  saturation: 1.0
  gamma: 1.0

scene:
  show_animation_progress: false
  leave_progress_bars: false
  preview_while_skipping: true
  default_wait_time: 1.0

vmobject:
  default_stroke_width: 4.0
  default_stroke_color: "#DDDDDD"   # GREY_A
  default_fill_color: "#888888"     # GREY_C

mobject:
  default_mobject_color: "#FFFFFF"  # WHITE
  default_light_color: "#BBBBBB"    # GREY_B

tex:
  template: "default"
  font_size_for_unit_height: 144

text:
  font: "Consolas"
  alignment: "LEFT"
  font_size_for_unit_height: 144

embed:
  exception_mode: "Verbose"
  autoreload: false

resolution_options:
  low: (854, 480)
  med: (1280, 720)
  high: (1920, 1080)
  4k: (3840, 2160)

sizes:
  frame_height: 8.0
  small_buff: 0.1
  med_small_buff: 0.25
  med_large_buff: 0.5
  large_buff: 1.0
  default_mobject_to_edge_buff: 0.5
  default_mobject_to_mobject_buff: 0.25

key_bindings:
  pan_3d: "d"
  pan: "f"
  reset: "r"
  quit: "q"          # Together with command/ctrl
  select: "s"
  unselect: "u"
  grab: "g"
  x_grab: "h"
  y_grab: "v"
  z_grab: "z"
  resize: "t"
  color: "c"
  information: "i"
  cursor: "k"

colors: { ...full named-color palette... }

log_level: "INFO"
universal_import_line: "from manimlib import *"
ignore_manimlib_modules_on_reload: true
```

> [!IMPORTANT]
> Tuple-typed values (`resolution`, `position`, `size`,
> `resolution_options.*`) are written as YAML strings of the form
> `(W, H)` and parsed by `ast.literal_eval` in
> `update_window_config` / `update_camera_config` / `get_resolution_from_args`.
> A bare YAML list (`[1920, 1080]`) will not work for these keys.

### 3.3 `directories`

| Key | Type | Default | Description |
|---|---|---|---|
| `mirror_module_path` | bool | `False` | When True, output paths mirror the source `.py` file's path (with `_` prefix in path components stripped). |
| `base` | str | `""` | Project root prepended to every subdir. Empty string means current working directory. |
| `subdirs.output` | str | `"videos"` | Where rendered videos and images are written. |
| `subdirs.raster_images` | str | `"raster_images"` | Directory `Mobject`s search when given a raster image filename. |
| `subdirs.vector_images` | str | `"vector_images"` | SVG asset search path. |
| `subdirs.three_d_models` | str | `"three_d_models"` | OBJ / GLTF model search path. |
| `subdirs.sounds` | str | `"sounds"` | Audio file search path for `Scene.add_sound`. |
| `subdirs.data` | str | `"data"` | Generic project data (CSVs, NumPy arrays). |
| `subdirs.downloads` | str | `"downloads"` | Where remote URL-fetched assets land. |
| `subdirs.latex_cache` | str | `"latex_cache"` | Compiled LaTeX SVG cache; relative to `base`. |
| `cache` | str | `""` | Override path for `Tex` / `Text` SVG cache. Empty string means use `appdirs.user_cache_dir("manim")`. |

`update_directory_config` resolves each subdir to `os.path.join(base, sub)`
in place, so after init the same dict has keys `directories.output`,
`directories.raster_images`, etc., as absolute paths.

### 3.4 `window`

| Key | Type | Default | Description |
|---|---|---|---|
| `position_string` | str | `"UR"` | Anchor on screen: two characters, where each is `U`/`O`/`D` (vertical) and `L`/`O`/`R` (horizontal). `"UR"` is upper-right. |
| `monitor_index` | int | `0` | Which monitor (0-based) to open the window on. |
| `full_screen` | bool | `False` | Forced full-screen on the chosen monitor. |
| `position` | str | unset | Pixel `(x, y)` of upper-left corner; overrides `position_string`. |
| `size` | str | unset | Pixel `(w, h)`; default is half of monitor width. |

### 3.5 `camera`

| Key | Type | Default | Description |
|---|---|---|---|
| `resolution` | str (tuple) | `"(1920, 1080)"` | Default render resolution. CLI overrides via `-l`/`-m`/`--hd`/`--uhd`/`-r`. |
| `background_color` | str | `"#333333"` | Hex or named color used by `Camera.clear`. |
| `fps` | int | `30` | Render frame rate. CLI overrides via `--fps`. |
| `background_opacity` | float | `1.0` | 0.0 = transparent. CLI `-t` sets this to 0.0 and switches codec to `prores_ks`. |

`Camera.__init__` also accepts `samples` (multisample anti-aliasing
levels), which `Scene` controls via the class-level attribute
`Scene.samples` (default `0`). Set this to `4` on a `ThreeDScene` if you
see jaggy edges on 3D surfaces.

### 3.6 `file_writer`

| Key | Type | Default | Description |
|---|---|---|---|
| `ffmpeg_bin` | str | `"ffmpeg"` | Binary name or absolute path. |
| `video_codec` | str | `"libx264"` | FFmpeg codec name. CLI `--vcodec` overrides. |
| `pixel_format` | str | `"yuv420p"` | FFmpeg pixel format. CLI `--pix_fmt` overrides. CLI `-t` clears it. |
| `saturation` | float | `1.0` | Post-render saturation adjustment via FFmpeg's `eq` filter. |
| `gamma` | float | `1.0` | Post-render gamma. |

> [!IMPORTANT]
> Setting `--vcodec prores_ks` plus `-t` (transparent) emits a `.mov`
> with alpha. ManimGL automatically applies this combination when only
> `-t` is passed. Override `pixel_format` only if you know what you're
> doing — yuv420p is required by most consumer players.

### 3.7 `scene`

| Key | Type | Default | Description |
|---|---|---|---|
| `show_animation_progress` | bool | `False` | Show a tqdm progress bar per animation. |
| `leave_progress_bars` | bool | `False` | Keep tqdm bars on screen after they finish. |
| `preview_while_skipping` | bool | `True` | When skipping animations (e.g. with `-s`), still draw one frame at the end of each `play` so the window stays alive. |
| `default_wait_time` | float | `1.0` | Default duration of `Scene.wait()` when called with no argument. |

Several CLI flags also write to this section indirectly:

- `-s` / `--skip_animations` -> `scene.skip_animations = True`.
- `-n N[,M]` -> `scene.start_at_animation_number = N`, `end_at_animation_number = M`.
- `-p` / `--presenter_mode` -> `scene.presenter_mode = True`.
- `--leave_progress_bars` -> `scene.leave_progress_bars = True`.
- `--show_animation_progress` -> `scene.show_animation_progress = True`.

### 3.8 `vmobject` and `mobject`

These two stanzas seed module-level constants in `manimlib/constants.py`:

- `vmobject.default_stroke_width` -> `DEFAULT_STROKE_WIDTH` (`4.0` ⇒ ~4 px at 1080p).
- `vmobject.default_stroke_color` / `default_fill_color` -> `DEFAULT_VMOBJECT_STROKE_COLOR` / `DEFAULT_VMOBJECT_FILL_COLOR`.
- `mobject.default_mobject_color` / `default_light_color` -> `DEFAULT_MOBJECT_COLOR` / `DEFAULT_LIGHT_COLOR`.

Override these on a per-mobject basis by passing `stroke_color=...`,
`fill_color=...` to constructors.

### 3.9 `tex` and `text`

`tex.template` selects from `manimlib/tex_templates.yml`. The shipped
templates include:

- **Classical:** `default` (LaTeX), `ctex` (xeLaTeX with Chinese support), `basic`, `basic_ctex`, `empty`, `empty_ctex`.
- **Font-specific (xelatex / lualatex / latex with mathastext):** `american_typewriter`, `antykwa`, `apple_chancery`, `auriocus_kalligraphicus`, `baskervald_adf_fourier`, `baskerville_it`, `biolinum`, `brushscriptx`, `chalkboard_se`, `chalkduster`, `comfortaa`, `comic_sans`, `droid_sans`, `droid_sans_it`, `droid_serif`, `droid_serif_px_it`, plus `ecf_*`, `electrum_adf`, `epigrafica`, `fourier_utopia`, `french_cursive`, `gfs_bodoni`, `gfs_didot`, `gfs_neohellenic`, `gnu_freesans_tx`, `gnu_freeserif_freesans`, `helvetica_fourier_it`, `latin_modern_tw`, and many more — over 90 templates total. Browse `manimlib/tex_templates.yml` for the full list.

Each template has fields:

```yaml
<name>:
  description: "..."
  compiler: latex | xelatex | lualatex
  preamble: |-
    \usepackage{...}
```

`text.font` is passed to Pango via `manimpango`; the system font lookup
applies. `text.alignment` is one of `LEFT` / `CENTER` / `RIGHT`.

### 3.10 `embed`

| Key | Type | Default | Description |
|---|---|---|---|
| `exception_mode` | str | `"Verbose"` | IPython's `xmode` setting. `Plain`/`Verbose`/`Context` are the standard values. |
| `autoreload` | bool | `False` | Re-import the user module on every IPython cell. CLI `--autoreload` sets this. |

### 3.11 `key_bindings`

Single-character key bindings consumed by `Scene` and `InteractiveScene`
event handlers. The defaults (`d` for 3D pan, `f` for 2D pan, `r` to
reset, `g`/`h`/`v`/`z` for grab and axis-lock, etc.) are read directly
from this dict at scene init — change them in `custom_config.yml` to
remap.

### 3.12 `colors`

64 named colors available as Python constants after `from manimlib import
*`. The standard 11-hue palette is `BLUE`, `TEAL`, `GREEN`, `YELLOW`,
`GOLD`, `RED`, `MAROON`, `PURPLE`, `GREY`, plus `WHITE`, `BLACK`. Each
hue has 5 shades suffixed `_A` (lightest) to `_E` (darkest). Bare
`BLUE`, `TEAL`, etc. alias to `_C` (median). Specials: `GREY_BROWN`,
`DARK_BROWN`, `LIGHT_BROWN`, `PINK`, `LIGHT_PINK`, `GREEN_SCREEN`,
`ORANGE`, `PURE_RED`, `PURE_GREEN`, `PURE_BLUE`. The full palette is
exposed as `MANIM_COLORS`. The `COLORMAP_3B1B = [BLUE_E, GREEN, YELLOW, RED]`
is the channel's signature gradient.

### 3.13 Top-Level Settings

| Key | Type | Default | Description |
|---|---|---|---|
| `log_level` | str | `"INFO"` | One of `DEBUG`/`INFO`/`WARNING`/`ERROR`/`CRITICAL`. CLI `--log-level` overrides. |
| `universal_import_line` | str | `"from manimlib import *"` | Code executed by `BlankScene.construct()` and on `manimgl` with no file. |
| `ignore_manimlib_modules_on_reload` | bool | `True` | Skip ManimGL itself when `reload_scene()` deep-reloads user modules. Avoids slow re-imports. |

## 4. Deployment and Scaling

ManimGL is a desktop / single-machine renderer. There is no built-in
client-server mode, no GPU offload, no distributed task queue. To run it
at scale, treat each scene as a CLI invocation and orchestrate externally.

### 4.1 Headless Rendering

`Camera.init_context` calls `moderngl.create_standalone_context()` when no
window is attached, so ManimGL renders without an X server. On Linux
headless boxes you typically still need:

- A working OpenGL driver (Mesa via `libgl1-mesa-glx` + `libegl1`).
- Either `xvfb-run` to provide a virtual display **or** EGL — moderngl's
  standalone context picks the right backend per platform.
- FFmpeg, LaTeX (`texlive-full`), and a font set installed.

A typical Dockerfile for batch rendering:

```dockerfile
FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
    ffmpeg texlive-full dvisvgm \
    libegl1 libgl1 libgles2 \
    libpango-1.0-0 libpangocairo-1.0-0 \
    fonts-dejavu fonts-liberation \
 && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir manimgl
WORKDIR /work
ENTRYPOINT ["manimgl"]
```

Then:

```bash
docker run --rm -v "$PWD":/work my-manimgl scene.py MyScene -w
```

### 4.2 CI Pipelines

Render on push or schedule:

```yaml
- name: Render scenes
  run: |
    pip install manimgl
    for scene in MyScene OtherScene; do
      manimgl scenes.py "$scene" -w --hd --fps 30
    done
- uses: actions/upload-artifact@v4
  with:
    name: videos
    path: videos/
```

> [!TIP]
> CI runners rarely have hardware acceleration. EGL software rasterization
> still works but is much slower than a real GPU. Budget 5x-10x the
> wall-clock you see locally.

### 4.3 Render Farms

A simple parallel pattern: one scene per worker. Each worker invocation
is independent (the global `manim_config` is rebuilt from scratch on each
process start). Use the same `custom_config.yml` across workers and a
shared output volume.

```bash
# GNU parallel
parallel -j 8 manimgl scenes.py {} -w --hd ::: SceneA SceneB SceneC ...
```

### 4.4 Embedding in a Larger Application

ManimGL is a Python library — you can drive it from any Python program
without going through the CLI:

```python
from manimlib import *
from manimlib.config import manim_config

# Mutate config in place
manim_config.camera.fps = 60
manim_config.file_writer.write_to_movie = True
manim_config.file_writer.output_directory = "/tmp/out"

# Build a scene
class Programmatic(Scene):
    def construct(self):
        self.play(ShowCreation(Circle()))

# Run it
scene = Programmatic()
scene.run()
```

This bypasses `parse_cli` entirely — set every config key yourself before
instantiating the scene.

## 5. Operations

### 5.1 Output Layout

After a successful render, expect:

```
{base}/
+- videos/
|   +- {scene_name}.mp4
|   +- {scene_name}.png            (when -s -w used)
|   +- {scene_name}/               (when --subdivide)
|       +- 00001_{anim_name}.mp4
|       +- 00002_{anim_name}.mp4
|       +- ...
+- latex_cache/
+- raster_images/                  (asset search path)
+- vector_images/
+- sounds/
+- ...
```

Plus the system cache at `appdirs.user_cache_dir("manim")` (or
`directories.cache`) holding compiled LaTeX SVGs.

### 5.2 Logging

Logs go to stderr via `rich.logging.RichHandler` (defined in
`manimlib/logger.py`). The logger name is `"manimgl"`. Set
`log_level: "DEBUG"` in `custom_config.yml` to see module-loader and
shader-compilation messages.

### 5.3 Cache Maintenance

The Tex/Text SVG cache lives at `directories.cache` (or
`appdirs.user_cache_dir("manim")` if empty). On a long-running production
machine, the cache can grow into the GB range over months. Two options:

- **Wipe interactively:** `manimgl --clear-cache` (calls
  `manimlib.utils.cache.clear_cache`).
- **Wipe scripted:** delete the directory directly. ManimGL recreates it
  on next run.

### 5.4 Pre-flight Checklist

Before a long batch render:

- [ ] `manimgl --version` runs and prints a green banner.
- [ ] `ffmpeg -version` runs (and the codec you need is listed under `ffmpeg -codecs`).
- [ ] `latex --version` runs (only if any scene uses `Tex`/`TexText`).
- [ ] `custom_config.yml` resolved: `python -c "from manimlib.config import manim_config; print(manim_config.camera, manim_config.directories)"` shows the merged values.
- [ ] Disk has free space for expected output (1080p @ 30fps libx264 averages ~5-15 MB/min; 4K can be 10x that).
- [ ] No two parallel workers share the same `custom_config.yml` directory unless they write to disjoint output subdirs.
- [ ] LaTeX cache pre-warmed: render a small representative scene first to populate it.

### 5.5 Monitoring a Running Render

Each `Scene.play(...)` call is a single FFmpeg pipe. While it runs:

- Use `--show_animation_progress` to get per-animation tqdm bars.
- Use `--prerun` to compute total framecount up front and get a global
  progress bar from the first frame.
- The OpenGL window (when present) shows the current frame in real-time.

> [!NOTE]
> ManimGL does not expose a programmatic progress hook. To integrate with
> an external job tracker (e.g. Celery beat, GitHub Actions step
> summaries), parse stdout for tqdm output or wrap the `Scene` with a
> custom subclass that overrides `play` to update a side channel.

## 6. Performance

### 6.1 Where Time Goes

| Stage | Typical share of frame time |
|---|---|
| Animation interpolation (Python) | 10-30% |
| Updater callbacks | 5-25% (depends on user code) |
| Mobject -> shader uniform packing | 5-15% |
| OpenGL draw calls | 30-60% (dominates 3D scenes) |
| FBO blit + read for FFmpeg | 5-15% (only when writing to file) |
| FFmpeg encode | runs concurrently with the next frame; rarely a bottleneck for libx264 at 1080p30 |

### 6.2 Known Hot Spots

- **`always_redraw(Constructor, *args)`** rebuilds a fresh mobject every
  frame. For deep mobjects (Tex with hundreds of glyphs), this is
  expensive. Prefer `mobject.add_updater(...)` that mutates an existing
  mobject in place.
- **`TransformMatchingStrings` / `TransformMatchingShapes`** do
  per-submobject pairing and longest-substring matching at `begin()`.
  Cost is O(n*m) in submobject counts; cap source/target string length.
- **`VShaderWrapper.get_fill_canvas`** allocates a 2x-resolution
  floating-point texture for fill compositing. Consider lowering
  `camera.resolution` for previews.
- **`Tex` first-render** triggers a LaTeX compile + dvisvgm + parse +
  glyph triangulation. After the first render, results are cached.
- **`Scene.assemble_render_groups`** runs after every `add` / `remove`
  to batch mobjects with the same shader. Adding hundreds of mobjects
  one at a time is slower than `self.add(*mobjects)` once.

### 6.3 Tuning Levers

| Lever | Where | Effect |
|---|---|---|
| `-l` / `-m` / `--hd` / `--uhd` | CLI | Resolution preset. Lower = faster. |
| `camera.fps` / `--fps` | YAML / CLI | 30 -> 60 doubles work. |
| `Scene.samples` | Subclass attribute | MSAA level for 3D. 0 = off. |
| `--prerun` | CLI | Adds one full pre-pass; use only when you need a global progress bar. |
| `--subdivide` | CLI | One movie file per animation. Slightly slower (more file opens) but resilient — a crash mid-scene loses only one animation. |
| Resolution of 3D surfaces | Mobject kwarg | `Sphere(resolution=(50, 100))` controls subdivisions. |
| `Tex(...)` cache | OS cache dir | First render is slow; subsequent are instant. |

### 6.4 Profiling

ManimGL has no built-in profiler. Wrap a scene in `cProfile`:

```python
import cProfile, pstats
from manimlib import *

class Bench(Scene):
    def construct(self):
        ...

scene = Bench()
prof = cProfile.Profile()
prof.runcall(scene.run)
pstats.Stats(prof).sort_stats("cumulative").print_stats(40)
```

Hot frames usually show `Mobject.update`, `Animation.interpolate_submobject`,
`VShaderWrapper.render_fill`, and FFmpeg pipe writes near the top.

## 7. Security

ManimGL is a developer tool intended to run **trusted Python source**. It
imports user files via `importlib.util.spec_from_file_location` and
executes them. Treat any `.py` you pass to `manimgl` as code you would
run via `python <file>` — there is no sandbox.

### 7.1 Threat Model

| Vector | Status |
|---|---|
| Untrusted scene file | **Not safe.** A scene file can do anything any Python program can do. Don't render `.py` files from strangers. |
| Untrusted YAML config | **Mostly safe.** ManimGL uses `yaml.safe_load`, so `!python/object` injection is prevented. However, several values (`(W, H)` tuples) go through `ast.literal_eval` — that allows literal int/float/string/tuple/list/dict only, not calls. |
| Untrusted assets (images, sounds, SVGs) | **Probably safe.** Image and sound paths are loaded via Pillow / pydub. Both have had security advisories — keep them updated. |
| Untrusted LaTeX strings | **Variable risk.** LaTeX itself can read/write files via `\write18` if `--shell-escape` is enabled. ManimGL does not pass `-shell-escape` to the LaTeX binary (see `manimlib/utils/tex_file_writing.py`), so the standard exploit chain is broken, but a malicious package included in the preamble could still misbehave. |
| Network access | **Possible** when a Mobject (e.g. `TexturedSurface`) is given a URL. Files are downloaded to `directories.downloads`. There is currently no allowlist or signature check. |
| Sound playback via shell | **Historic concern.** Issue #2434 (closed) reported a command-injection vector via PowerShell sound playback on Windows and SSRF via unrestricted URL fetching. Verify your version postdates the fix. |

> [!WARNING]
> Issue #2434 surfaced a command-injection in Windows sound playback and
> an SSRF in URL-asset fetching. The closed-status of the issue suggests
> a fix landed, but if you operate ManimGL on a multi-tenant / shared
> system, audit `manimlib/utils/sounds.py:play_sound` and the
> URL-fetching code path before exposing it to externally-supplied
> scene files.

### 7.2 Hardening for Multi-Tenant Use

If you must run user-supplied scenes (e.g. a "render-as-a-service"
product), put each render inside an isolated container or sandbox:

- Run ManimGL as a non-root user with no write access outside a
  per-job temp dir.
- Drop network access (`--network=none`) unless you specifically allow
  remote-asset URLs.
- Enforce a wall-clock and memory limit on the FFmpeg + Python
  processes.
- Use `seccomp` / `apparmor` profiles to block `/etc/passwd`-style
  reads if you treat the source as untrusted.

### 7.3 Cache Poisoning

The Tex/Text cache hashes the source string and template into a content
key. A rogue scene file that produces the same hash as a previously
rendered cache entry is an unlikely (~2^256-bit) collision concern, not a
realistic attack. There is no hostname, user, or signing data in the
cache key beyond content.

## 8. Developer Internals

### 8.1 Repository Layout

```
manim/                                          (3b1b/manim repo root)
+- example_scenes.py            All canonical demos in one file
+- requirements.txt             Runtime Python deps
+- pyproject.toml               Build metadata (setuptools)
+- setup.py                     Trivial setuptools.setup() shim
+- setup.cfg                    Static metadata (name=manimgl, etc.)
+- MANIFEST.in                  Includes shaders, YAMLs, examples in sdist
+- LICENSE.md                   MIT
+- README.md                    Brief — points users to ManimCE for general use
+- logo/
|   +- logo.py                  The 3B1B logo as a runnable scene
|   +- *.png                    Pre-rendered logo assets
+- docs/
|   +- Makefile / make.bat      Sphinx wrappers
|   +- requirements.txt         Sphinx==3.0.3, sphinx-copybutton, furo, Jinja2
|   +- example.py
|   +- source/
|       +- conf.py
|       +- index.rst
|       +- manim_example_ext.py     Custom Sphinx directive: ManimExampleDirective
|       +- documentation/*.rst       Per-module API reference
|       +- getting_started/*.rst     Quickstart, install, configuration, examples
|       +- development/*.rst         Changelog, contributing, about
|       +- _static/icon.png
+- manimlib/                    The package
    +- __init__.py              Re-exports the entire public API
    +- __main__.py              `python -m manimlib` entry point
    +- config.py                CLI parsing + YAML merge -> manim_config
    +- default_config.yml       Hard-coded defaults
    +- constants.py             Frame size, vectors, named colors, angles
    +- logger.py                rich-backed logger "manimgl"
    +- module_loader.py         Hot-reload aware module import
    +- extract_scene.py         Scene discovery + BlankScene
    +- shader_wrapper.py        ShaderWrapper, VShaderWrapper, fill canvas
    +- tex_templates.yml        100+ LaTeX preamble bundles
    +- typing.py                Type aliases (ManimColor, Vect3, ...)
    +- window.py                Pyglet OpenGL window + event delegation
    +- animation/               13 files; Animation base + every concrete subclass
    +- camera/                  Camera, ThreeDCamera, CameraFrame
    +- event_handler/           EventDispatcher singleton
    +- mobject/                 17 top-level files + svg/, types/ subpackages
    |   +- mobject.py           Mobject base, Group, Point, _AnimationBuilder
    |   +- types/               VMobject, Surface, DotCloud, ImageMobject, PMobject
    |   +- svg/                 Tex, TexText, Text, Brace, drawings
    |   +- ...                  Geometry, matrix, numbers, etc.
    +- scene/                   Scene, InteractiveScene, embed shell, file writer
    +- shaders/                 GLSL programs (vert/geom/frag) + helper inserts
    |   +- quadratic_bezier/    Stroke / fill / depth shaders for VMobject
    |   +- surface/             3D surface shaders
    |   +- textured_surface/    UV-textured surface shaders
    |   +- mandelbrot_fractal/  Two named fractal demos
    |   +- newton_fractal/
    |   +- true_dot/            High-quality round-dot shader
    |   +- image/               2D image shader
    |   +- inserts/             Reusable GLSL snippets included via #include
    +- utils/                   19 files; bezier, color, file ops, rate funcs, ...
```

### 8.2 The `Scene` Lifecycle

```python
scene = SceneClass(**scene_config)
scene.run()
  +- file_writer.begin()
  +- setup()                          # subclass hook
  +- construct()                      # user-supplied animation script
  +- interact()                       # only if window != None; idle event loop
  +- tear_down()
      +- stop_skipping()
      +- file_writer.finish()
      +- if window: window.destroy()
```

`construct()` is what users override. It can call `self.play(...)`,
`self.wait(...)`, `self.add(...)`, `self.remove(...)`, `self.embed()`,
and any custom logic.

Inside `play`:

```
play(*proto_animations, run_time=None, rate_func=None, lag_ratio=None)
  -> animations = [prepare_animation(a) for a in proto_animations]
  -> for anim in animations: anim.update_rate_info(...)
  -> pre_play()
  -> begin_animations(animations)
  -> progress_through_animations(animations)   # the inner time loop
  -> finish_animations(animations)
  -> post_play()
```

### 8.3 Mobject Class Hierarchy

```
object
+- Mobject                      mobject/mobject.py
|   +- Group                    mobject/mobject.py
|   +- Point                    mobject/mobject.py
|   +- VMobject                 mobject/types/vectorized_mobject.py
|   |   +- VGroup               mobject/types/vectorized_mobject.py
|   |   +- VHighlight           mobject/types/vectorized_mobject.py
|   |   +- TipableVMobject      mobject/geometry.py
|   |   |   +- Arc -> Circle -> Dot, Ellipse, SmallDot
|   |   |   +- Arc -> ArcBetweenPoints -> CurvedArrow -> CurvedDoubleArrow
|   |   |   +- Line -> DashedLine, TangentLine, StrokeArrow, Arrow -> Vector
|   |   +- Polygon -> Rectangle -> Square -> ...
|   |   +- ParametricCurve -> FunctionGraph
|   |   +- ImplicitFunction
|   |   +- StringMobject (svg/string_mobject.py)
|   |   |   +- Tex (svg/tex_mobject.py)
|   |   |   |   +- TexText
|   |   |   +- Text (svg/text_mobject.py)
|   |   +- DecimalNumber -> Integer
|   |   +- Matrix -> DecimalMatrix -> IntegerMatrix; TexMatrix; MobjectMatrix
|   |   +- VectorField -> TimeVaryingVectorField
|   |   +- ...
|   +- Surface                  mobject/types/surface.py
|   |   +- Sphere, Torus, Cylinder, Cone, Disk3D, Square3D, ...
|   |   +- TexturedSurface
|   +- DotCloud                 mobject/types/dot_cloud.py
|   |   +- TrueDot
|   +- ImageMobject             mobject/types/image_mobject.py
|   +- PMobject                 mobject/types/point_cloud_mobject.py
|   +- ValueTracker             mobject/value_tracker.py
|       +- ExponentialValueTracker, ComplexValueTracker
+- CoordinateSystem (ABC)       mobject/coordinate_systems.py
    +- Axes  (also inherits VGroup)
        +- ThreeDAxes
        +- NumberPlane
            +- ComplexPlane
+- _AnimationBuilder            (internal, returned by mobject.animate)
+- _UpdaterBuilder              (internal, returned by mobject.always)
+- _FunctionalUpdaterBuilder    (internal, returned by mobject.f_always)
```

The `CameraFrame` class (in `manimlib/camera/camera_frame.py`) inherits
from `Mobject` so the camera itself is animatable.

### 8.4 Animation Class Hierarchy

```
object
+- Animation                                  animation/animation.py
    +- AnimationGroup                         animation/composition.py
    |   +- Succession
    |   +- LaggedStart -> LaggedStartMap
    |   +- Flash, AnimationOnSurroundingRectangle, FlashyFadeIn, ...
    +- ShowPartial                            animation/creation.py
    |   +- ShowCreation -> Uncreate
    |   +- ShowPassingFlash
    +- DrawBorderThenFill -> Write
    +- ShowIncreasingSubsets -> ShowSubmobjectsOneByOne, AddTextWordByWord
    +- Transform                              animation/transform.py
    |   +- ReplacementTransform
    |   +- TransformFromCopy
    |   +- MoveToTarget -> _MethodAnimation
    |   +- ApplyMethod
    |   |   +- ApplyPointwiseFunction
    |   |   |   +- ApplyMatrix
    |   |   +- FadeToColor
    |   |   +- ScaleInPlace -> ShrinkToCenter
    |   |   +- ApplyComplexFunction
    |   +- Restore
    |   +- ApplyFunction
    |   +- ApplyPointwiseFunctionToCenter
    |   +- CyclicReplace -> Swap
    |   +- FocusOn, Indicate, CircleIndicate, TurnInsideOut
    +- Fade -> FadeIn -> FadeInFromPoint
    |     `- FadeOut -> FadeOutToPoint
    +- FadeTransform -> FadeTransformPieces
    +- VFadeIn -> VFadeOut, VFadeInThenOut
    +- GrowFromPoint -> GrowFromCenter, GrowFromEdge, GrowArrow
    +- VShowPassingFlash -> FlashAround -> FlashUnder
    +- ShowCreationThenDestruction
    +- ShowCreationThenFadeOut (Succession)
    +- WiggleOutThenIn
    +- ApplyWave (Homotopy)
    +- Homotopy                               animation/movement.py
    |   +- SmoothedVectorizedHomotopy, ComplexHomotopy
    +- PhaseFlow
    +- MoveAlongPath
    +- Rotating -> Rotate                     animation/rotation.py
    +- ChangingDecimal                        animation/numbers.py
    |   +- ChangeDecimalToValue, CountInFrom
    +- Broadcast (LaggedStart)                animation/specialized.py
    +- TransformMatchingParts                 animation/transform_matching_parts.py
    |   +- TransformMatchingShapes
    |   +- TransformMatchingStrings -> TransformMatchingTex
    +- UpdateFromFunc, UpdateFromAlphaFunc    animation/update.py
    +- MaintainPositionRelativeTo
```

### 8.5 The Renderer

ManimGL's renderer is shader-based and pure-OpenGL. The lifecycle:

1. **Camera setup.** `Camera.__init__` builds a `CameraFrame` (a `Mobject`
   subclass with Euler-angle uniforms), a `moderngl` context (window-shared
   if a window is attached, otherwise a standalone EGL/WGL context), and
   two FBOs — `fbo_for_files` (the offscreen one used when writing video)
   and `draw_fbo` (the read-back FBO for grabbing pixels).
2. **Per-mobject shader programs.** Each Mobject type declares which
   `shader_folder` to use (e.g. `"quadratic_bezier"`, `"surface"`,
   `"true_dot"`). `ShaderWrapper.init_program` compiles the GLSL files in
   that folder into an OpenGL program. `VShaderWrapper` is a specialized
   subclass for vector mobjects: it compiles three programs (stroke,
   fill, depth) and uses two passes per frame.
3. **Render groups.** `Scene.assemble_render_groups` clusters adjacent
   mobjects of the same `(type, shader_id, z_index)` so they can share a
   draw call.
4. **Frame capture.** `Scene.update_frame` calls
   `self.camera.capture(*self.render_groups)`. Each render group's
   `pre_render` updates uniforms, binds textures, sets depth-test and
   clip-plane state; then `render` issues the draw call.
5. **Output.** If a movie is being written, `Scene.emit_frame` calls
   `SceneFileWriter.write_frame(camera)`, which reads pixels from
   `draw_fbo` (`Camera.get_raw_fbo_data`) and writes them to FFmpeg's
   stdin.

#### 8.5.1 The Fill Compositing Trick

`VShaderWrapper.get_fill_canvas` is one of ManimGL's signature techniques.
It constructs a separate floating-point texture (size: 2x camera
resolution, components=4, dtype `f2`) used as an intermediate fill canvas.
The fill shader writes signed alpha values, and the composite shader
resolves overlapping triangles via a winding-number computation:

```glsl
if (color.a < 0) {
    color.a = -color.a / (1.0 - color.a);
    color.rgb *= (color.a - 1);
}
```

This is what enables ManimGL to render arbitrary self-intersecting
polygons with correct winding fill, rather than relying on stencil tricks
or libtess.

#### 8.5.2 Stroke Rendering

Strokes use the `quadratic_bezier/stroke` GLSL programs. Each `VMobject`
is decomposed into quadratic Bezier curves, packed into a vertex buffer
with attributes `(point, stroke_rgba, stroke_width, joint_angle,
unit_normal)`, and rendered through a geometry shader that emits triangles
along the curve. Joint angles let the geometry shader produce correct
miter / bevel joints without a CPU pre-pass.

#### 8.5.3 Depth Test

Set on a per-mobject basis via `ShaderWrapper.set_ctx_depth_test`. 3D
mobjects pass `depth_test=True`; 2D mobjects skip it. Mobjects can also
set up to four GLSL clip planes by writing `clip_plane0..clip_plane3`
into their uniform dict.

### 8.6 Configuration Layering Algorithm

```python
def initialize_manim_config() -> Dict:
    args = parse_cli()
    global_defaults = load_yaml(<package>/manimlib/default_config.yml)
    project_overrides = load_yaml("custom_config.yml")             # CWD
    file_overrides = load_yaml(args.config_file) if args.config_file else {}

    config = Dict(merge_dicts_recursively(
        global_defaults, project_overrides, file_overrides
    ))

    log.setLevel(args.log_level or config["log_level"])

    update_directory_config(config)
    update_window_config(config, args)
    update_camera_config(config, args)
    update_file_writer_config(config, args)
    update_scene_config(config, args)
    update_run_config(config, args)
    update_embed_config(config, args)

    return config
```

`merge_dicts_recursively` (in `manimlib/utils/dict_ops.py`) is a deep
dict merge — child dicts merge key-by-key, scalars in later sources
overwrite earlier ones, lists overwrite (not concatenate).

The `Dict` (from `addict`) wrapping turns every key into an attribute,
so `config.camera.fps` works as well as `config["camera"]["fps"]`.

### 8.7 Module Loader and Hot Reload

`manimlib/module_loader.py:ModuleLoader` is the layer that imports
user-supplied scene files. The non-trivial part is supporting
`reload_scene()` from inside the IPython shell.

```
ModuleLoader.get_module(file_name, is_during_reload):
    if file_name is None:                       # `manimgl` with no file
        return None                             # caller will use BlankScene
    spec = importlib.util.spec_from_file_location(...)
    module = importlib.util.module_from_spec(spec)
    if is_during_reload:
        # Track every import the module triggers,
        # then deep-reload all user-defined ones.
        imported = _exec_module_and_track_imports(spec, module)
        _reload_modules(imported, set())
    spec.loader.exec_module(module)
    return module
```

`_is_user_defined_module` filters out stdlib (anything under
`sysconfig.get_path("stdlib")`), site-packages / dist-packages, and
ManimGL itself if `ignore_manimlib_modules_on_reload=True`. The deep
reload walks the imported module's `__dict__` for nested modules and
class-attached modules, calling `importlib.reload` on each.

### 8.8 Interactive Shell (`InteractiveSceneEmbed`)

Defined in `manimlib/scene/scene_embed.py`. On `Scene.embed()` it:

1. Stops skipping animations.
2. Forces one frame draw so the window has something to show.
3. Builds an `InteractiveSceneEmbed`, which:
   - Creates an IPython terminal `InteractiveShell` and registers a
     custom exception handler that flashes a red full-screen rectangle
     when a cell raises.
   - Pre-populates the shell namespace with `self`, every local in
     `construct`, and a curated set of helpers (`play`, `add`, `remove`,
     `clear`, `wait`, `save_state`, `restore`, `touch`, `reload_scene`,
     `auto_reload`, `checkpoint_paste`).
   - Hooks `pre_run_cell` so each cell runs `Scene.update_frame` between
     interactions, keeping the window responsive.
4. Calls `shell.embed()` to enter the REPL.
5. Raises `EndScene` when the user exits, ending the run loop cleanly.

`reload_scene()` validates syntax of the user file, sets
`run_config.is_reload = True`, and runs the `exit_raise` IPython magic to
break out of the shell. The outer `run_scenes` loop in `__main__.py`
catches `KillEmbedded` and starts the scene again from a clean slate
(but with the same `embed_line`, optionally overridden).

`CheckpointManager` provides "checkpoint paste":

- Read a Python snippet from the system clipboard (`pyperclip.paste()`).
- If the snippet's first non-whitespace line is a comment, use that
  comment as a checkpoint key. Save the scene state on first use; on
  subsequent uses, restore to the saved state before running the
  snippet.
- This lets you iterate on a block of scene code without rewinding the
  whole scene.

### 8.9 The File Writer Subprocess

`SceneFileWriter` (in `manimlib/scene/scene_file_writer.py`) manages the
FFmpeg subprocess. Its lifecycle:

```
begin_animation()              # opens a new ffmpeg pipe (or partial movie)
  +- writing_process = subprocess.Popen([
       "ffmpeg",
       "-y", "-loglevel", "error",
       "-f", "rawvideo",
       "-s", f"{w}x{h}",
       "-pix_fmt", "rgba",
       "-r", str(fps),
       "-i", "-",                      # video frames from stdin
       *audio_inputs,
       "-c:v", video_codec,
       "-pix_fmt", pixel_format,
       movie_path,
     ], stdin=PIPE)

write_frame(camera)            # one .write() per frame
  +- writing_process.stdin.write(camera.get_raw_fbo_data())

end_animation()                # close pipe, optionally append to master file
finish()                       # final cleanup
```

If `--subdivide` is set, each animation gets its own movie file under a
folder named after the scene. Otherwise all animations share a single
`{scene}.mp4`.

`add_sound(sound_file, time, gain=None, gain_to_background=None)`:

```python
if time < 0:
    raise Exception("Adding sound at timestamp < 0")
```

Audio is loaded via `pydub.AudioSegment`, stamped with `time_offset`,
mixed via `pydub.effects.normalize`, and the result is written as a
sidecar WAV that FFmpeg picks up at finalization time.

### 8.10 Event Handler Internals

`manimlib/event_handler/event_dispatcher.py` defines a singleton
`EVENT_DISPATCHER` (constructed at module import). Mobjects can subscribe
to events by attaching listeners:

```python
from manimlib.event_handler import EVENT_DISPATCHER
from manimlib.event_handler.event_listner import EventListener
from manimlib.event_handler.event_type import EventType

listener = EventListener(my_mobject, EventType.MousePressEvent, my_callback)
EVENT_DISPATCHER.add_listner(listener)
```

`EventType` is an `enum.Enum` with values: `MouseMotionEvent`,
`MousePressEvent`, `MouseReleaseEvent`, `MouseDragEvent`,
`MouseScrollEvent`, `KeyPressEvent`, `KeyReleaseEvent`.

Dispatch logic in `EventDispatcher.dispatch`:

- **Mouse events**: only fire on listeners whose mobject's bounding box
  contains the mouse point (`is_point_touching`). Drag events fire only
  on the listeners that were touching at the corresponding `MousePress`.
- **Key events**: fire on every listener for the key event type.
- A listener returns `False` to stop propagation; otherwise other
  listeners get called.

`Window.on_mouse_motion` / `on_mouse_drag` / `on_key_press` etc. forward
to the active scene, which forwards (via `EVENT_DISPATCHER.dispatch`) to
the mobject listeners and then handles built-in interactions
(camera pan, scroll-zoom, undo/redo).

Concrete usage: the `MotionMobject`, `Button`, `Checkbox`,
`LinearNumberSlider`, `ColorSliders`, `Textbox`, and `ControlPanel`
classes in `manimlib/mobject/interactive.py` all install event listeners
in their `__init__`.

### 8.11 LaTeX Pipeline

`manimlib/utils/tex_file_writing.py` orchestrates `Tex` / `TexText`
compilation:

```
latex_to_svg(tex_string, template_name="default"):
    template = get_tex_template_config(template_name)   # from tex_templates.yml
    full_tex = wrap_in_template(tex_string, template)   # adds preamble
    ...build full_tex.tex, .aux, .log...
    sp.run([template.compiler, "-no-pdf", ...])         # latex / xelatex / lualatex
    sp.run(["dvisvgm", "--no-fonts", ...])              # converts dvi -> svg
    return svg_path
```

Errors raise `LatexError` (the only public custom exception in the utils
package). The cache key is `hash_string(full_tex)`, and the cached SVG
lives at `<cache_dir>/<hash>.svg`. Cache hits skip the compile step.

`get_tex_template_config(name)` reads the requested template from
`manimlib/tex_templates.yml`. If you maintain your own preamble bundles,
either add them to a fork of `tex_templates.yml` or pass a custom
`tex.template` value pointing to a key you've added.

> [!IMPORTANT]
> ManimGL does **not** pass `--shell-escape` to the LaTeX binary. This
> blocks the standard `\write18{...}` command-injection vector. If you
> add a custom template, do not enable shell-escape unless you control
> every `Tex` string the renderer will see.

### 8.12 Code Patterns and Conventions

- **`from __future__ import annotations`** at the top of nearly every
  module. Type hints are PEP 604 / generic syntax; many are lazy-loaded
  in `if TYPE_CHECKING:` blocks to avoid circular imports.
- **`addict.Dict`** is used in place of dict-with-defaults for config.
  Reading a missing attribute returns an empty `Dict()`, not a
  `KeyError`. This is convenient but can mask typos — be careful.
- **Classes prefer `__init__`-time configuration** to subclass attributes
  for "user-tweakable" behavior (`Scene.random_seed`, `Scene.samples`,
  `InteractiveScene.palette_colors`). Callers override these as class
  attributes on their subclass; constructor args are reserved for
  per-instance values.
- **Animations construct a `starting_mobject` copy in `begin()`** and
  interpolate from `starting_mobject` -> `mobject`. Don't mutate
  `starting_mobject` after `begin` returns.
- **`@affects_mobject_list`** (a method decorator on `Scene`) calls
  `self.assemble_render_groups()` after the wrapped method returns,
  refreshing the renderer's batches. Apply it to any `Scene` method that
  changes `self.mobjects`.
- **Method chaining returns `self`** on most setters. `mob.set_color(RED).shift(UP)` works.
- **No `@property` setters** on Mobjects — use `set_*` methods. This is
  an explicit choice so that setters can do extra work (resampling
  bezier curves, refreshing bounding boxes) without surprising users.
- **GLSL shaders are textual `.glsl` files** loaded by
  `manimlib/utils/shaders.py:get_shader_code_from_file` from
  `manimlib/shaders/`. The path-resolution rules support
  `#include "common.glsl"` directives via the `inserts/` folder.

### 8.13 Testing Strategy

The repo does not ship a comprehensive automated test suite. Validation
is essentially:

- Running `example_scenes.py` end-to-end on each release.
- Visual review of rendered output.
- The `3b1b/videos` companion repo serving as a real-world regression
  surface (Grant uses ManimGL daily).

If you fork ManimGL and want CI coverage, consider:

- Snapshot-test individual mobjects: render a scene with `-s -w` to a
  PNG and image-diff against a golden.
- Lint with `ruff` or `flake8`. The codebase doesn't enforce a style
  but PEP 8 fits.
- `mypy --check-untyped-defs` runs but produces many warnings — ManimGL
  uses type hints liberally without enforcing them strictly.

### 8.14 Extending ManimGL

#### Add a new Mobject

1. Subclass `Mobject` (point cloud), `VMobject` (vector graphics), or
   `Surface` (3D mesh).
2. Implement `init_points(self)` to populate `self.points`.
3. Optionally override `init_colors`, `init_uniforms`, `init_data`.
4. If you need a custom shader, add a folder under `manimlib/shaders/`
   with `vert.glsl`, `geom.glsl` (optional), `frag.glsl` and pass its
   name to `Mobject.shader_folder`.
5. Re-export from `manimlib/__init__.py` if you want it imported by
   `from manimlib import *`.

#### Add a new Animation

1. Subclass `Animation`, `Transform`, `AnimationGroup`, or another base.
2. For the simplest case, override `interpolate_submobject(self, sub,
   sub_starting, alpha)` to define how each submobject changes.
3. For animations that need a per-frame setup, override
   `interpolate_mobject(self, alpha)`. Compute `alpha` via
   `self.time_spanned_alpha(alpha)` if you want `time_span` to work.
4. Override `begin`, `finish`, or `clean_up_from_scene` if you need
   lifecycle hooks.
5. Place the file under `manimlib/animation/` and re-export.

#### Add a new CLI flag

1. Add `parser.add_argument(...)` in `manimlib/config.py:parse_cli`.
2. Decide where the flag should land in `manim_config`. Typically you
   add an `update_<section>_config(config, args)` block near the
   existing ones.
3. Reference the new key from the relevant subsystem (camera, file
   writer, scene). Updates are visible to all `Scene` instances created
   during that run.

### 8.15 Potential Improvements

These are gaps an interested contributor might address. They are
observations, not promises:

- **Add a frozen-time progress callback**. `Scene.play` exposes no hook
  for external tools; a job-queue integration must parse stdout. A
  simple `progress_callback: Callable[[int, int], None]` parameter on
  `Scene` would unblock orchestration tooling.
- **Move the fill canvas to a smaller resolution for previews**. The
  2x-resolution `f2` texture is overkill for a 480p preview and costs
  significant VRAM and bandwidth.
- **Replace the regex-based GLSL `#include` resolver** in
  `utils/shaders.py:get_shader_code_from_file` with a proper preprocessor
  pass — the current one cannot handle conditional includes or nested
  defines.
- **Add a deterministic test surface** (snapshot tests for at least
  every example scene at a low resolution) to catch shader regressions.
- **Tighten `addict.Dict` usage**. It silently materializes empty dicts
  for missing keys, which masks typos like `manim_config.cammera.fps`.
  A wrapping class that whitelists known keys would catch these at
  startup.
- **Document the asset URL fetching path**. `TexturedSurface(url, url)`
  silently downloads — there should be a `directories.allowed_hosts`
  allowlist for multi-tenant deployments (relates to issue #2434).
- **Add `bash-completion` for the `manimgl` CLI**. The argparse spec is
  large enough that tab completion would meaningfully help.

### 8.16 Contributing

The repository's high-level workflow:

1. Fork `https://github.com/3b1b/manim` on GitHub.
2. Branch off `master`. Keep PRs small and focused.
3. Match the existing style: PEP 8 spacing, type hints with
   `from __future__ import annotations`, `import` ordering by stdlib /
   third-party / first-party / local, no unnecessary docstrings on
   trivial methods (the codebase favors expressive method names over
   docstrings).
4. Open a PR. Grant reviews them as time permits — bear in mind the
   project is a small group's tool, not a community staffed library.
5. For visual changes, attach a rendered before/after MP4 or GIF.

`docs/source/development/contributing.rst` has the canonical instructions
and any current style nits.

## 9. Integration Points

ManimGL is a self-contained desktop application but it does interact with
the host environment in a few well-defined ways:

| Integration | How | Source |
|---|---|---|
| **FFmpeg** | Subprocess pipe (raw RGBA frames over stdin) | `manimlib/scene/scene_file_writer.py` |
| **LaTeX (`latex` / `xelatex` / `lualatex`)** | Subprocess (one call per `Tex` cache miss) | `manimlib/utils/tex_file_writing.py` |
| **`dvisvgm`** | Subprocess (DVI -> SVG) | `manimlib/utils/tex_file_writing.py` |
| **Pango / HarfBuzz** | In-process via `manimpango` for `Text` glyph layout | `manimlib/mobject/svg/text_mobject.py` |
| **OpenGL driver** | Via `moderngl` (`moderngl_window` for windowed; standalone EGL/WGL for headless) | `manimlib/camera/camera.py`, `manimlib/window.py` |
| **System clipboard** | `pyperclip` (read in `CheckpointManager`, write in `InteractiveScene`) | `manimlib/scene/scene_embed.py`, `manimlib/scene/interactive_scene.py` |
| **System cache dir** | `appdirs.user_cache_dir("manim")` | `manimlib/utils/cache.py`, `tex_file_writing.py` |
| **Filesystem** | YAML config from CWD; assets from `directories.*` paths | `manimlib/utils/file_ops.py`, `directories.py` |
| **HTTP(S)** | Asset URL downloads via `validators` + stdlib `urllib` | `manimlib/utils/file_ops.py` (and surrounding utils) |
| **Audio** | `pydub` reads sound files; FFmpeg muxes them onto the final movie | `manimlib/utils/sounds.py` |
| **IPython** | Embedded shell for interactive development | `manimlib/scene/scene_embed.py` |

There is no:

- Built-in HTTP server, API, or RPC layer.
- Plugin system (the package re-exports `manimlib.*`; "plugins" in
  the ManimGL world are just Python modules you import alongside your
  scenes).
- Database or persistent state beyond the on-disk cache.
- Telemetry, analytics, or call-home behavior.

> [!NOTE]
> If you need to integrate ManimGL with another system (a web service,
> an LMS, a video editor), wrap the CLI. The library's API is stable
> enough to script directly, but the boundary is "import the package and
> instantiate a Scene", not "call into an exposed service".

---

_Generated April 2026 from 3b1b/manim at c5e23d9. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
