Manim Community Edition (Python)
The community-maintained fork of 3Blue1Brown's manim: a retained-mode, CPU-vector 2D animation engine that turns a Scene.construct method into an MP4 by rasterizing a cubic-Bézier scene graph with Cairo and muxing the frames through PyAV.
Last reviewed: July 11, 2026
| Field | Value |
|---|---|
| Language | Python (requires-python >=3.11, pyproject.toml:23) |
| License | MIT (Copyright (c) 2024, the Manim Community Developers, LICENSE.community) |
| Repository | ManimCommunity/manim (reviewed at 4d25c031, v0.20.1) |
| Documentation | docs.manim.community |
| Category | Retained-mode 2D vector animation engine (imperative scene-graph → video) |
| First release | v0.1.0 — October 21, 2020, forked from 3b1b/manim (0.1.0-changelog.rst) |
| Default renderer | Cairo (CPU vector, config.renderer == RendererType.CAIRO); optional OpenGL/moderngl |
| Bézier basis | Cubic (Cairo VMobject, n_points_per_cubic_curve = 4); quadratic (OpenGLVMobject, = 3) |
| Video encoder | PyAV (import av → libav/FFmpeg): libx264/yuv420p default; libvpx-vp9, qtrle for webm/transparent |
NOTE
Every file:line on this page is against the local clone at 4d25c031 (v0.20.1). The manim project ships two renderers with two geometry models in one tree; unqualified statements below describe the default Cairo path, and the OpenGL divergences are called out explicitly. The sibling deep-dive manimgl covers 3Blue1Brown's upstream, which is quadratic-and-GPU throughout.
Overview
What it solves
manim is a library for programmatic explanatory-math animation: you subclass Scene, build mathematical objects (Mobjects) in a construct method, and call self.play(Animation(...)) to advance time. The README states the scope plainly:
"Manim is an animation engine for explanatory math videos. It's used to create precise animations programmatically, as demonstrated in the videos of 3Blue1Brown." —
README.md
The engine's job is the whole path from that Python description to an encoded video: a retained scene graph of vector objects, a time-sampled interpolation loop, a CPU vector rasterizer, a LaTeX/Pango typesetting front-end, and a content-hash cache that skips re-rendering unchanged play() calls.
Design philosophy
Manim CE began as a cleanup fork rather than a rewrite — the first changelog is explicit:
"This is the first release of manimce after forking from 3b1b/manim. As such, developers have focused on cleaning up and refactoring the codebase while still maintaining backwards compatibility wherever possible." —
0.1.0-changelog.rst
Three architectural commitments follow from that lineage and are load-bearing for anything that reimplements the engine:
- Everything on screen is a
Mobject. The base class (mobject.py:72) is described as "Mathematical Object: base class for objects that can be displayed on screen" — a tree ofsubmobjectswith apointsarray. Geometry, text, and groups are all the same node type. - Vector-first, cubic-Bézier geometry. A displayable shape is a
VMobject: a flatpointsarray grouped into 4-point cubic Bézier curves, rasterized by Cairo. Nothing is a bitmap until the final frame. - A dual-backend metaclass, chosen at import.
ConvertToOpenGL(opengl_compatibility.py:17) is a "Metaclass for swapping (V)Mobject with its OpenGL counterpart at runtime depending on config.renderer" — so the same user class name resolves to a Cairo (cubic) or an OpenGL (quadratic) base class depending onconfig.
How it works
A render is a straight-line pipeline. The CLI (manim render scene.py Scene, built on click/cloup, __main__.py:3) instantiates the Scene, runs construct, and each self.play(...) drives one iteration of the loop below.
# scene.py — the per-play() loop (CairoRenderer.play, cairo_renderer.py:64)
scene.compile_animation_data(*args, **kwargs) # build Animation list, split moving/static
hash_current_animation = get_hash_from_play_call( # content hash of this call
scene, self.camera, scene.animations, scene.mobjects)
if self.file_writer.is_already_cached(hash_current_animation):
self.skip_animations = True # cache hit → skip rendering
...
scene.play_internal() # sample time, render, encode
self.num_plays += 1play_internal (scene.py:1351) samples wall-clock time into frames, and for each frame sets every animation's progress and repaints:
# scene.py:1364 — deterministic frame sampling + interpolate + render
self.time_progression = self._get_animation_time_progression(self.animations, self.duration)
for t in self.time_progression: # times = np.arange(0, run_time, 1/frame_rate)
self.update_to_time(t) # alpha = t/run_time; animation.interpolate(alpha)
self.renderer.render(self, t, self.moving_mobjects)update_to_time (scene.py:1687) computes alpha = t / animation.run_time and calls animation.interpolate(alpha), which reshapes alpha through the rate_func, lerps each submobject's points and colors, and mutates the scene graph in place. renderer.render then calls camera.capture_mobjects (camera.py:528), which walks each VMobject into a Cairo path and fills/strokes it, and SceneFileWriter encodes the resulting RGBA framebuffer into a partial movie file — one per play() — through PyAV. finish() concatenates the partials into the final MP4.
Object & scene model
Manim is a retained-mode scene graph (../concepts.md): the Scene keeps a persistent list of Mobjects, animations mutate those objects between frames, and the renderer re-reads the whole graph each frame. Mobject (mobject.py:72) is a node holding self.submobjects: list[Mobject] (mobject.py:120) plus a points NumPy array and a color; its __init__ calls reset_points() / generate_points() / init_colors() (mobject.py:125-127) as empty template hooks subclasses fill in.
The submobject list is one-directional: add appends via self.submobjects = list_update(self.submobjects, unique_mobjects) (mobject.py:560) with no parent back-reference, so the "family" of a node is recomputed on demand by get_family (mobject.py:2518), which chains children recursively and de-duplicates (extract_mobject_family_members, family.py:12). New subclasses are wired through __init_subclass__ (mobject.py:100), which resets animation_overrides and captures _original__init__ per class — the hook that lets mob.animate.shift(...) dispatch to a per-type override.
The renderer choice is baked into the type via the ConvertToOpenGL metaclass (opengl_compatibility.py:28): when config.renderer == RendererType.OPENGL, it rewrites a class's bases through a name map ("Mobject": OpenGLMobject, "VMobject": OpenGLVMobject) so the same class Square(...) inherits the quadratic-GPU or cubic-Cairo lineage. The full object model, the cubic points layout, and the fill/stroke split are the subject of scene-graph.md.
Animation & timing model
An Animation (animation.py:30) is "An animation. Animations have a fixed time span." — a function of a scalar alpha ∈ [0,1] that mutates its mobject. Its __init__ (animation.py:128) carries the four timing knobs: run_time, rate_func (default smooth), reverse_rate_function, and lag_ratio. Each frame, interpolate_mobject (animation.py:339) zips the object's family members and applies a staggered sub-progress per member via get_sub_alpha (animation.py:364):
# animation.py:384 — lag_ratio staggering of submobject i of n
full_length = (num_submobjects - 1) * lag_ratio + 1
value = alpha * full_length
lower = index * lag_ratio
return self.rate_func(value - lower) # rate_func saturates outside [0,1]The rate function reshapes time (easing); it is not clamped here because every exported rate function is wrapped by the unit_interval decorator (rate_functions.py:124), which returns 0 for t < 0 and 1 for t > 1.
WARNING
The default rate_func is smooth (rate_functions.py:156), a normalized logistic sigmoid — min(max((sigmoid(inflection·(t-0.5)) - error) / (1 - 2·error), 0), 1) with inflection = 10.0 — not the smootherstep polynomial 6t⁵ − 15t⁴ + 10t³. That polynomial is a separate function, smootherstep (rate_functions.py:174). The rate-functions.d probe reimplements smootherstep as a self-contained stand-in for the sigmoid; both are monotone [0,1]→[0,1] S-curves, but the exact default is the sigmoid.
Composition lives in composition.py. AnimationGroup (composition.py:30) lays its children on a timeline via build_animations_with_timings (composition.py:146) — start[1:] = cumsum(run_times[:-1] * lag_ratio), end = start + run_times — so lag_ratio = 0 plays all together, 1 is strict succession (Succession, composition.py:198), and fractional values overlap (LaggedStart, composition.py:297). Transform (transform.py:58) is the workhorse morph: begin (transform.py:200) calls self.mobject.align_data(self.target_copy) to make point counts match, then interpolate_submobject (transform.py:249) lerps points along a path_func and blends color. See ../concepts.md and the rate-functions.d / affine-transform.d probes.
Rendering backend & rasterization
The default backend is Cairo, a CPU vector rasterizer (../concepts.md). Camera imports it directly (import cairo, camera.py:15) and binds a Cairo surface straight onto the NumPy pixel buffer:
# camera.py:591 — a Cairo context over the RGBA framebuffer
surface = cairo.ImageSurface.create_for_data(pixel_array.data, cairo.FORMAT_ARGB32, pw, ph)
ctx = cairo.Context(surface)
ctx.set_matrix(cairo.Matrix(pw/fw, 0, 0, -(ph/fh), ...)) # world → pixel affinedisplay_vectorized (camera.py:677) draws each object as stroke(background) → fill → stroke, and set_cairo_context_path (camera.py:698) feeds Manim's cubic tuples to Cairo's cubic path API one curve at a time: ctx.curve_to(*p1[:2], *p2[:2], *p3[:2]) (camera.py:727). Fill is ctx.fill_preserve() (camera.py:781) — Cairo does its own scan-conversion and winding, so Manim never triangulates for the Cairo path. Anti-aliasing is Cairo's, not Manim's. The framebuffer is 4-channel RGBA (image_mode = "RGBA", n_channels = 4, camera.py:87), and renderer.get_frame returns np.array(self.camera.pixel_array) (cairo_renderer.py:181).
The optional OpenGL backend (OpenGLRenderer, moderngl) is a GPU vector renderer: its OpenGLVMobject is quadratic (n_points_per_curve = 3, opengl_vectorized_mobject.py:112) with quadratic_bezier_fill/quadratic_bezier_stroke shaders, and fill is GPU-triangulated by earclip_triangulation (space_ops.py:718, mapbox_earcut, space_ops.py:10). That earcut path is only reached from the OpenGL renderer (vectorized_mobject_rendering.py:162); the Cairo path never calls it. The two rasterization models — and why cubic-vs-quadratic is load-bearing — are detailed in scene-graph.md and the bezier-eval.d / frame-capture.d probes.
Typesetting & text
Text is also vector geometry — every glyph becomes VMobject outlines. Three front-ends feed one SVG-to-VMobject importer:
- LaTeX —
SingleStringMathTex(SVGMobject)(tex_mobject.py:46) and its subclassesMathTex/Texcalltex_to_svg_file(tex_file_writing.py:35), a multi-step subprocess pipeline: write.tex→compile_tex(asubprocess.runoflatex/pdflatex/lualatex/xelatex,tex_file_writing.py:214) →.dvi/.xdv/.pdf→convert_to_svgviadvisvgm --no-fonts(tex_file_writing.py:249, which turns glyphs into paths, not embedded fonts). - Pango —
Text/MarkupText(bothSVGMobject,text_mobject.py:302) shape non-math text throughmanimpango(import manimpango,text_mobject.py:66):manimpango.text2svg(...)(text_mobject.py:818) andMarkupUtils.text2svg(...)for markup (text_mobject.py:1367). - Typst —
typst_to_svg_file(typst_file_writing.py:35) compiles Typst markup in-process to SVG (typst_compiler.compile(..., format="svg"),typst_file_writing.py:100) — nodvisvgmintermediate — behind the optionaltypst>=0.14extra.
All three land in SVGMobject (svg_mobject.py, svgelements parser at svg_mobject.py:11), whose handle_commands (svg_mobject.py:561) walks each path segment and elevates lines and quadratics into the cubic points array (or lowers cubics to quads under OpenGL). The full LaTeX/Pango/Typst mechanics and glyph-outline extraction are in text-pipeline.md.
Output & encoding
Encoding is PyAV (import av, scene_file_writer.py:17) — the libav/FFmpeg Python bindings — with a distinctive one-partial-movie-file-per-play() scheme that is the physical substrate of the cache. open_partial_movie_stream (scene_file_writer.py:540) opens a container per call and picks the codec/pixel-format:
| Output | Codec | Pixel format | Source |
|---|---|---|---|
default .mp4 | libx264 | yuv420p | scene_file_writer.py:552 |
.webm | libvpx-vp9 | yuv420p | scene_file_writer.py:560 |
.webm transparent | libvpx-vp9 | yuva420p | scene_file_writer.py:562 |
transparent .mov | qtrle | argb | scene_file_writer.py:566 |
Frames are pushed as av.VideoFrame.from_ndarray(frame, format="rgba") (scene_file_writer.py:449) and muxed on a writer thread. In finish(), combine_files writes an FFmpeg concat list and re-opens it with av.open(str(file_list), format="concat") (scene_file_writer.py:652) to produce the final video, stamped Rendered with Manim Community v{__version__}. Subtitles use srt (srt.compose, scene_file_writer.py:898); PNG/GIF are alternate finish() branches. See ../concepts.md and frame-capture.d.
Interactivity, preview & authoring
Manim is fundamentally a batch renderer, not a live tool. The default flow renders to disk and then opens the file: --preview/-p triggers open_media_file (file_ops.py:220) after the render completes. There is no live Cairo window.
Live interaction is OpenGL-only. Scene.interactive_embed (scene.py:1412) asserts isinstance(self.renderer, OpenGLRenderer) (scene.py:1415) and drops into an IPython embedded shell over the moderngl window; the Cairo backend has no equivalent. Authoring surfaces: a %%manim Jupyter magic (ipython_magic.py), Scene.next_section (scene.py:340) for chaptered output (section.py), and a checkhealth CLI subcommand that verifies the LaTeX/FFmpeg toolchain. The CLI subcommand set is cfg, checkhealth, init, plugins, render (manim/cli/).
Extensibility & API surface
The user-facing API is subclassing + method chaining: subclass Scene, override construct, and compose Mobject subclasses whose methods return self. Extension points:
config—ManimConfig(_config/utils.py:145) is aMutableMappingdescribed as "a single source of truth for all of the library's customizable behavior", layeringdefault.cfg(configparser) < CLI < programmatic assignment;tempconfig(_config/__init__.py:46) scopes overrides.RendererType(constants.py:258) is thecairo/openglswitch.- Animation overrides —
Mobject.__init_subclass__+animation_override_forlet aMobjectsubclass supply a custom animation formob.animate. - Plugins — a
manim pluginssubcommand and amanim.pluginspackage discover third-partyMobject/Sceneextensions. - Custom
Mobjects — overridegenerate_pointsto fill thepointsarray; the base hooks are deliberately empty (mobject.py:468).
Dependencies (from pyproject.toml) name the whole backend: pycairo, av, manimpango, svgelements, skia-pathops (boolean path ops, boolean_ops.py:8), mapbox-earcut, numpy>=2.1, scipy, networkx, pillow, pygments, and moderngl/moderngl-window (all required, though moderngl is only exercised by the OpenGL renderer). Only typst, gui (dearpygui), and jupyterlab are [project.optional-dependencies] extras.
Determinism, caching & performance
Two orthogonal determinism mechanisms make caching correct.
Deterministic frame sampling. get_time_progression (scene.py:1087) samples times = np.arange(0, run_time, 1 / config["frame_rate"]) — a fixed grid independent of wall-clock speed (../concepts.md), so a given animation always produces the same frames at the same t.
Per-play() content-hash caching. Before rendering, CairoRenderer.play computes get_hash_from_play_call (hashing.py:333) over the camera, the animations, and the current mobjects. The docstring defines the key exactly:
"A string concatenation of the respective hashes of
camera_object,animations_listandcurrent_mobjects_list, separated by_." —hashing.py:358
Each component is a zlib.crc32 of a custom JSON serialization (hashing.py:366); run-dependent fields are stripped via KEYS_TO_FILTER_OUT = {"original_id", "background", "pixel_array", "pixel_array_to_cairo_context"} (hashing.py:29) — flagged in-source as elements "not suitable for hashing (too long or run-dependent)" (hashing.py:27). The hash is the partial-movie filename (f"{hash_animation}{movie_file_extension}", scene_file_writer.py:270), so is_already_cached (scene_file_writer.py:606) is a single path.exists(), and eviction is LRU by st_atime capped at max_files_cached (default 100, scene_file_writer.py:864). The full mechanism, including the circular-reference _Memoizer and the determinism it depends on, is in caching.md.
Performance profile. The Cairo path is single-threaded per frame; the encoder runs on a background writer thread. The static/moving split (begin_animations, scene.py:1334) paints non-moving objects once into a static image, so only moving_mobjects are re-rasterized per frame.
Strengths
- Uniform object model. Everything is a
Mobjectwith apointsarray; geometry, text, and groups compose through one tree and oneinterpolate. - Cubic-Bézier vector geometry feeds Cairo directly (
ctx.curve_to) with no approximation — the on-screen curve is exactly the stored curve. - Correct, cheap caching. Deterministic sampling + a content hash that is the filename makes cache hits a
path.exists()check; unchangedplay()calls cost nothing. - Rich typesetting. LaTeX, Pango markup, and Typst all reduce to vector outlines, so math and text animate identically to shapes.
- Batteries-included encoding. PyAV covers MP4/WebM/GIF/PNG and transparent output without shelling out to a separate
ffmpegbinary.
Weaknesses
- Two geometry models in one tree. Cubic (Cairo) vs quadratic (OpenGL) means a curve is stored differently per backend, and the cubic→quadratic lowering is lossy (
opengl_vectorized_mobject.py:499). - CPU-bound default renderer. Cairo rasterizes one frame at a time on one thread; complex scenes are slow, and the OpenGL alternative is less mature.
- Heavy external toolchain. LaTeX text needs a full TeX install +
dvisvgmas subprocesses; a missing binary fails at render time. - No live editing on the default backend. Interactivity requires the OpenGL renderer; Cairo is render-then-open.
- Fork drift. API-compatible with
3b1b/manimonly "wherever possible"; ManimGL and Manim CE have diverged substantially (seemanimgl).
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
Retained scene graph of Mobjects | One uniform node type; animations mutate persistent objects | Whole graph re-read per frame; no immediate-mode escape hatch |
| Cubic Bézier basis on the Cairo default | Maps 1:1 to Cairo's curve_to; exact on-screen curves | Diverges from the quadratic OpenGL/ManimGL basis; lossy interchange |
Dual backend via ConvertToOpenGL metaclass | Same user code targets CPU-vector or GPU-vector by config | Two geometry models, two rasterizers, two fill strategies to maintain |
| Cairo (CPU) as default rasterizer | Deterministic, dependency-light, high-quality AA out of the box | Single-threaded per frame; GPU speed only via the less-mature OpenGL path |
| Text = vector outlines (LaTeX/Pango/Typst → SVG) | Math and text animate exactly like shapes; resolution-independent | Needs external TeX/dvisvgm subprocesses; slow first compile per string |
PyAV per-play() partial movie files | Each call is an independent, cacheable, concatenatable clip | Many small files; a concat pass and cache eviction to manage |
| Content-hash cache keyed by the filename | Cache hit is a path.exists(); deterministic sampling makes it sound | Hashing must strip run-dependent state; a missed field silently over-caches |
Sources
Primary (local clone ManimCommunity/manim@4d25c031, v0.20.1):
manim/mobject/mobject.py—Mobjectbase class, family tree,interpolate,ConvertToOpenGLwiring.manim/mobject/types/vectorized_mobject.py— cubicVMobject(n_points_per_cubic_curve = 4), fill/stroke arrays,align_points.manim/mobject/opengl/opengl_compatibility.py·opengl_vectorized_mobject.py— the quadratic OpenGL lineage.manim/animation/animation.py·composition.py·transform.py— timing,lag_ratio, composition, morphs.manim/utils/rate_functions.py—smooth(sigmoid) and friends.manim/renderer/cairo_renderer.py·manim/camera/camera.py— the Cairo render loop and vector rasterization.manim/scene/scene.py·scene_file_writer.py— the play loop, time sampling, PyAV encoding, partial-movie caching.manim/utils/hashing.py—get_hash_from_play_call,_Memoizer.manim/utils/tex_file_writing.py·typst_file_writing.py·manim/mobject/text/·manim/mobject/svg/svg_mobject.py— the typesetting front-ends and SVG-to-VMobjectimporter.manim/_config/utils.py·manim/constants.py·default.cfg·pyproject.toml— config, renderer enum, dependencies.
Official docs: docs.manim.community · reference repository.
Deep sub-topics: scene graph & geometry · text pipeline · caching & determinism. Shared vocabulary: ../concepts.md. Sibling engine: manimgl. Runnable probes: bezier-eval.d · rate-functions.d · affine-transform.d · frame-capture.d.