Skip to content

MathAnimation (C++)

A from-scratch, single-binary, native OpenGL Manim-like engine with a Dear ImGui timeline editor and real-time GUI+audio preview — the closest existing analog to what a native D animation engine would be, and therefore the most decision-relevant reference in this survey.

FieldValue
LanguageC++17 (set(CMAKE_CXX_STANDARD 17)), C11 for vendored C (CMakeLists.txt)
LicenseCustom restrictive EULA (EULA.txt, adapted from Aseprite's) — repo has no OSI license (gh api … licensenull)
Repositorygithub.com/ambrosiogabe/MathAnimation (created 2021-07-24; 1016★, 51 forks; author GamesWithGabe)
DocumentationNone beyond README.md; the author's YouTube channel is the only tutorial surface
CategoryNative offline math-animation renderer + real-time GUI-timeline editor; retained scene
RendererCustom OpenGL (glad + GLFW) — hybrid: CPU plutovg path fills → GPU texture atlas; GL miter-tessellated strokes for write-on
EditorDear ImGui (imgui) docked panels: Timeline, Scene, Inspector, Animations, Export Video, gizmos, wireframe/filled
Video outputLinked SVT-AV1 encoder → AV1 in an IVF container (DKIF/AV01); default filename .mov; not ffmpeg, not mp4
TypesettingFreeType glyph-outline extraction; LaTeX by shelling out to MiKTeX latex + dvisvgm → SVG (same pipeline as Manim's latex→svg)
PlatformWindows-first (README lists only Windows); Linux paths exist in-tree (PlatformLinux.cpp, find_package(OpenSSL)) but are secondary
Latest activitymaster HEAD 4b2bace5, 2024-02-03 ("add syntax highlighting"); repo last pushed 2024-09-08; no tags, no releases

NOTE

Every claim on this page is source-verified against ambrosiogabe/MathAnimation@4b2bace5 (the current master HEAD), read file-by-file — not from the README's feature list, which is partly aspirational (it advertises "mp4" export and animations marked NOT IMPLEMENTED/BROKEN/STATUS UNKNOWN). Where the running code and the README disagree, the code wins and the discrepancy is flagged.


Overview

What it solves

MathAnimation is a bespoke tool one developer (GamesWithGabe) built to make the explanatory-math animations in his YouTube videos — a native, self-contained desktop application that produces Manim-style vector animations but adds the one thing Manim's batch renderer lacks: a real-time, scrub-able GUI with audio preview. You build a scene of animation objects (text, LaTeX, SVG imports, shapes, code blocks), drag animations onto a multi-track timeline, scrub/play the result live, and export a video file. It is a single C++ executable — no Python, no interpreter, no external render server — which is exactly the shape a native D engine would take.

Design philosophy

The README states the goal directly (README.md):

"My goal is to have nearly identical animations to those produced by Manim, except in realtime with a GUI+audio preview to enhance the editing process."

That single sentence sets the whole design: match Manim's output, invert Manim's workflow. Manim is code-first and batch-rendered; MathAnimation is GUI-first and interactively previewed, with the same underlying primitives — VMobject-style piecewise-Bézier SVG paths, Create/Transform/ FadeIn-style animations, LaTeX via dvisvgm. The trade the author accepts for real-time preview is a GPU rasteriser (not bit-reproducible) plus a CPU plutovg fill cache — a pragmatic hybrid rather than Manim-community's deterministic Cairo oracle. The licensing philosophy is equally deliberate: the source is open to read and compile, but redistribution is forbidden (README.md):

"This library is free for you to compile and modify for your own personal use, but it is not free for you to distribute any binary copies (paid or free)."


How it works

Language, build & dependencies

MathAnimation is C++17, built with CMake (cmake_minimum_required(VERSION 3.21), README says 3.16+) into one executable target, MathAnimations (Animations/CMakeLists.txt). There is no package manager: every dependency is a git submodule vendored under Animations/vendor/ and compiled from source into the binary (.gitmodules). The full third-party set:

SubmoduleRole
GLFW, gladWindow/context creation and OpenGL function loading
dearimguiThe entire editor UI (docking branch, custom InternalImGuiConfig.h)
freetypeFont loading and glyph-outline extraction
plutovgCPU 2D vector rasteriser — fills SVG paths to pixels
tinyxml2XML parsing for .svg file import
onigurama (oniguruma)Regex engine backing TextMate-grammar syntax highlighting
luauRoblox's typed Lua — the scripting/extensibility layer
nlohmann/jsonProject-file (.json) serialization
av1 (SVT-AV1)The video encoder
openal (OpenAL-soft)Audio playback for the timeline's waveform preview
nativeFileDialogNative open/save dialogs
glm, cppUtils, stbMath, the author's own utilities, image load/save
optickOptional profiler (only in the RelWithProfiler config)
themes/*, grammars/*VSCode .tmLanguage/theme JSON copied in as assets at build time

The only non-vendored system dependency is OpenSSL on Linux (find_package(OpenSSL REQUIRED)OpenSSL::Crypto, for MD5 hashing) (Animations/CMakeLists.txt). The build is warnings-as-errors (/W4 /WX on MSVC, -Wall -Wextra -Wpedantic -Werror elsewhere). This "vendor everything, compile once" model is precisely the single-binary discipline a D engine would inherit — dub would pull each of these as a git dependency or ImportC shim rather than a submodule.

The animation/timeline model

The runtime is a retained scene of AnimObjects keyed by ID (AnimObjId), plus a list of Animations that reference objects by ID and are laid out on a multi-track timeline. An Animation is a plain struct (Animation.h):

cpp
struct Animation {
    AnimTypeV1 type;
    int32 frameStart;          // timeline position, in integer FRAMES
    int32 duration;            // length, in integer FRAMES
    int32 timelineTrack;
    EaseType easeType;         // Linear/Sine/Quad/…/Elastic/Bounce
    EaseDirection easeDirection;
    PlaybackType playbackType; // Synchronous | LaggedStart
    float lagRatio;
    std::unordered_set<AnimObjId> animObjectIds;
    union { /* per-type payload */ } as;
    void applyAnimation(AnimationManagerData* am, float t = 1.0f) const;
};

Time is measured in integer frames, not seconds — the timeline is a grid of frames and applyAnimation(am, t) drives object state from an interpolation t ∈ [0,1]. The animation catalog (AnimTypeV1) is a direct Manim echo: MoveTo, Create, UnCreate, FadeIn, FadeOut, Transform (labelled "Replacement Transform"), Circumscribe, AnimateScale, plus stubs (RotateTo, AnimateStrokeColor/FillColor/StrokeWidth, Shift) the README marks NOT IMPLEMENTED/BROKEN. Object types (AnimObjectTypeV1) are TextObject, LaTexObject, Square, Circle, Cube, Axis, SvgObject, SvgFileObject, Camera, ScriptObject, CodeBlock, Arrow, Image. The whole scene serializes to a versioned nlohmann::json project file (current version 3), each object carrying a serialize/deserialize pair plus a [[deprecated]] legacy_deserialize for upgrading beta projects.

The renderer

The renderer is a custom OpenGL module with a NanoVG-style immediate path API layered over retained draw-list batching (Renderer.h): a push/pop style stack (pushColor, pushStrokeWidth, pushLineEnding, pushCamera2D/3D, pushFont) feeds beginPath/lineTo/quadTo/cubicTo/ endPath/renderOutline calls that build geometry into 2D/3D/line/font draw lists, flushed to a Framebuffer (FBO). Everything renders in 3D space even for 2D scenes — a design choice the source annotates with characteristic candour (SvgCache.cpp):

"// Everything is 3D now... Good or bad? Who knows?"

The load-bearing subtlety is that fill and stroke take different paths (detailed under Rendering backend): fills are rasterised on the CPU by plutovg into a GPU-resident texture atlas, while the animated write-on outline is tessellated to GL triangles.


Object & scene model

MathAnimation is squarely a retained-mode engine, and its object model maps cleanly onto the survey's Mobject vocabulary — with concrete C++ names. An AnimObject is the retained node (Animation.h); parent/child structure is expressed through AnimObjId links and a _isInternalObjectOnly flag that hides generated children (e.g. the per-object SvgObject a TextObject expands into). The vector-geometry workhorse is SvgObject (Svg.h): an array of Paths, each an array of Curves, each a tagged union over Line/Bezier2/Bezier3:

cpp
enum class CurveType : uint8 { None, Line, Bezier2, Bezier3 };
struct Curve { CurveType type; Vec2 p0; union { Line; Bezier2; Bezier3; } as; };
struct Path  { Curve* curves; int numCurves; bool isHole; };
struct SvgObject { Path* paths; int numPaths; Vec4 fillColor; FillType fillType; uint8* md5; /*…*/ };

Two facts matter for a D port. First, MathAnimation stores both quadratic (Bezier2) and cubic (Bezier3) curves natively in one path, rather than normalising to a single Bézier basis the way the Manim forks do — so its curve type is a per-segment tagged union, and its plutovg/FreeType/SVG importers each emit whichever degree the source provides (the bezier-eval.d probe evaluates both bases directly). Second, holes are an explicit isHole boolean on Path and fill obeys a FillType (NonZeroFillType/EvenOddFillType), so overlap is resolved by the winding rule, not by boolean path pre-processing. Each SvgObject carries an md5 of its binary path data — the key its render cache is content-hashed on. Affine placement is a per-object globalTransform (glm::mat4), the affine-transform model the affine-transform.d probe verifies.


Animation & timing model

On the execution-model axis MathAnimation is the GUI-timeline/keyframe pole: there is no self.play script and no reactive graph — you drag clips onto tracks and the engine samples the scene at a frame. Each Animation owns a [frameStart, frameStart+duration] window on a timelineTrack, an easeType+easeDirection, a playbackType (Synchronous or LaggedStart), and a lagRatio (Animation.h). applyAnimation(am, t) maps global time to a per-animation t ∈ [0,1], applies the easing via CMath::ease(t, type, direction), then interpolates object state.

The easing catalog (EaseType in CMath.h) is the Penner family — Linear, Sine, Quad, Cubic, Quart, Quint, Exponential, Back, Elastic, Bounce — each combinable with an EaseDirection (In/Out/InOut), the same easing space the rate-functions.d probe tabulates (though MathAnimation uses the Penner curves, not Manim's smootherstep default). Composition is expressed through PlaybackType: Synchronous runs an animation's target objects together, LaggedStart staggers them by lagRatio — the lag_ratio mechanism, realised as a per-animation enum + float rather than Manim's AnimationGroup/LaggedStart combinator tree. _isAnimationGroupData/_appliesToChildrenData tables mark which animation types (Create, UnCreate, FadeIn, FadeOut) recurse into a target's children.

The Transform ("Replacement Transform") morphs one SvgObject into another by interpolating control points, and Svg::interpolate(src, dst, t) is where the hard point-count alignment problem lives (Svg.cpp): the code splits and pads paths (Curve::split(t0, t1)de Casteljau subdivision — and Svg::beginPath on modified copies) so the source and destination have matching curve counts before a point-wise lerp — the single subtlest part of the engine, exactly as the concepts page warns.


Rendering backend & rasterization

This axis is MathAnimation's most instructive finding, because it does not pick a single side of the CPU-vector vs GPU-vector split — it runs a hybrid keyed on whether a shape is being filled or drawn on.

Fills go through plutovg on the CPU. SvgObject::render creates a plutovg_surface_t, and fillWithPluto replays the object's curves as plutovg path commands — plutovg_move_to, plutovg_cubic_to, plutovg_quad_to, plutovg_line_to — sets the fill rule (plutovg_fill_rule_even_odd / plutovg_fill_rule_non_zero) from FillType, and calls plutovg_fill_preserve (Svg.cpp). plutovg is an analytic CPU rasteriser, so this is coverage-based anti-aliasing, not MSAA. The resulting pixels are then uploaded to an OpenGL texture via texture.uploadSubImage. The shape is rasterised white and tinted at draw time:

"// Draw the SVG with full alpha since we apply alpha changes at the compositing level / Render the SVG in white then color it when blitting the texture to a quad" (Svg.cpp)

Strokes/outlines go through the GL renderer. The Create write-on effect (renderOutline2D) does not use plutovg — it emits the outline into the custom GL path API (Renderer::beginPath/lineTo/quadTo/cubicTo) up to a partial arc length t * approximatePerimeter, so the stroke grows as the animation plays (Svg.cpp). Those paths are tessellated to triangles on the CPU by a miter-join generator (generateMiter3D, which computes a miter normal from the previous/current/next points and expands the polyline by strokeWidth) and drawn as GL triangles (Renderer.cpp).

The fill results are held in an SVG texture-atlas cache (SvgCache.cpp): a 4096×4096 FBO with four color attachments, into which each rasterised SVG is packed shelf-style (cacheCurrentPos, cacheLineHeight) and drawn back as a drawTexturedQuad3D. When the atlas fills, an LRU cache (cachedSvgs) evicts the oldest entries that fit — or scissors and re-rasterises in place, or "grows" onto the next color attachment. This is a glyph-atlas pattern applied to whole vector objects. There is also a jump-flood shader pass (jumpFloodShader, renderStencilOutlineToFramebuffer) used for the editor's selection-glow outline. On the color/gamma axis, compositing/tinting happens at the quad-blit stage (fills stored as white RGBA), and the export path converts to YUV in a shader (below).

NOTE

A D engine gets a real decision here. MathAnimation demonstrates that a credible native engine can avoid writing a fill rasteriser at all — vendoring a small CPU vector library (plutovg is a single-purpose ~few-KLOC library) for fills, uploading to a GPU atlas, and reserving custom GL only for the animated stroke. That is a far smaller surface than a stencil-and-cover or analytic-SDF GPU fill, at the cost of a CPU→GPU upload per uncached shape.


Typesetting & text

Text and math are two separate in-process pipelines, both ending in SvgObject paths.

Fonts / glyph outlines. TextObjects are rendered by pulling vector outlines from FreeType, not by blitting a bitmap atlas for the scene. Fonts.cpp includes <freetype/ftoutln.h>, loads each glyph with FT_Load_Glyph(…, FT_LOAD_NO_SCALE), takes the FT_OutlineGlyph, and walks its contours into a GlyphOutline, classifying each point as on-curve vs off-curve (quadratic vs "third-order Bézier") via FT_getPointType (Fonts.cpp). Kerning comes from FT_Get_Kerning. (A parallel SizedFont/GlyphTexture path exists for rasterised UI text, but scene text is outline-based.) There is no HarfBuzz/Pango shaping — glyphs are advanced by their advanceX plus pairwise kerning, so complex-script shaping (Arabic, Indic, bidi) is out of scope, unlike Manim's manimpango.

LaTeX → SVG. MathAnimation reproduces Manim's classic pipeline exactly, by shelling out to a system LaTeX install (LaTexLayer.cpp). It looks for MiKTeX on PATH (miktex/bin/x64/latex.exe, …/dvisvgm.exe on Windows; miktex / miktex-dvisvgm elsewhere), wraps the user's markup in a \documentclass[preview]{standalone} preamble loading amsmath, amssymb, physics, etc. (MathTex additionally wraps it in \begin{align*}…\end{align*}), writes a .tex file, and runs two subprocesses:

cpp
// latex/<md5>.tex  →  latex.exe -halt-on-error  →  <md5>.dvi
Platform::executeProgram(latexProgram, (latexFilename + " -halt-on-error").c_str(), "./latex/", …);
// <md5>.dvi        →  dvisvgm -n                →  <md5>.svg
Platform::executeProgram(dvisvgmProgram, (dviFilename + " -n").c_str(), "./latex/", …);

The output .svg is content-hash cached on the MD5 of the LaTeX string (Platform::md5FromString), so identical equations are compiled once and reused across runs — the same disk cache Manim keeps, and the reason LaTeX objects need the miktex+dvisvgm toolchain present. Generation runs on a background thread, serialised one-at-a-time so the app doesn't fork a process storm.

SVG parsing. Imported .svg files are parsed with tinyxml2 (parseSvgDoc requires a <svg> element with a viewBox, walks <defs>/<g>/<style>, parses <path> and <polygon>, applies a small CSS Stylesheet) plus a hand-written d-attribute parser (parseSvgPath) covering M/L/H/V/C/Q/S/T/A and their relative forms (SvgParser.cpp).


Output & encoding

The output path is a purpose-built readback → YUV → AV1 pipeline, and it is not what the README ("Export the final animation as an mp4 file") advertises. The real mechanics (ExportPanel.cpp, Encoder.cpp):

  1. Fixed-timestep render. Export sets the play state to PlayForwardFixedFrameTime at a hard-coded 60 fps (getExportSecondsPerFrame() = 1/60), resets to frame 0, and steps frame-by-frame — the deterministic frame sampling discipline.
  2. GPU RGB→YUV pass. renderTextureToYuvFramebuffer converts the main framebuffer to planar YUV 4:2:0 across two FBOs — a full-resolution Y target and a half-resolution UV target (Renderer.h declares the RgbToYuvShader).
  3. Async readback. A PixelBufferDownload (OpenGL PBO) queues an asynchronous download of the Y/UV framebuffers, then hands the packed planar buffer to encoder->pushYuvFrame(...) — the readback-to-buffer step the frame-capture.d probe stands in for.
  4. Encode. VideoEncoder::startEncodingFile drives SVT-AV1 directly through its C API (svt_av1_enc_init_handle, svt_av1_enc_send_picture, svt_av1_enc_get_packet), configured crf 28, preset 12, color-format 420, input-depth 8. Frames are staged in a memory-mapped file, encoded on a worker thread, and muxed into an IVF container written by hand — the header literally begins DKIF … AV01:
cpp
unsigned char header[32] = { 'D','K','I','F', 0,0,32,0, 'A','V','0','1' };  // IVF, not MP4

Two honest caveats. The container is IVF/AV1, but the export dialog defaults the filename to .mov (filepath.replace_extension(".mov"), NFD_SaveDialog("mov", …)) — so a file with a .mov (or README-promised .mp4) name actually holds an IVF AV1 stream. And the alternative hardware path is unimplemented: NvidiaEncoder.cpp is a 0-byte empty file, and the export loop carries the TODO:

"// TODO: Add a hardware accelerated version that usee CUDA and NVENC" (ExportPanel.cpp)

During export, plutovg fills are forced synchronous (if (ExportPanel::isExportingVideo()) svg->render(...) vs renderAsync otherwise) at Ultra SVG fidelity, so no frame ships a half-rasterised cache entry (SvgCache.cpp).


Interactivity, preview & authoring

This is the axis MathAnimation exists for and where it most differs from Manim. The entire editor is Dear ImGui — a docking layout of panels (README.md enumerates them; source under Animations/src/editor/panels/ and …/timeline/):

  • Timeline — multi-track, zoomable; drag/drop animation clips, resize by dragging clip edges, magnet-toggle snapping, a draggable playhead on a frame ruler, and per-track audio waveform preview (OpenAL) for syncing to a voiceover.
  • Animation Editor View — the scrubbing viewport with gizmos to drag objects, scroll-to-zoom, middle-click-pan; Animation View is the clean final-render preview (space bar toggles play/stop).
  • Scene hierarchy (color-coded active/animating/inactive), Inspector (per-object and per-animation properties), Animations palette, Asset Manager (scripts), Console (script logs/errors, click-to-open), Export Video, App Metrics (FPS), Editor Settings (wireframe/filled, camera sensitivity).

There is an UndoSystem and clipboard, a CodeEditorPanel with TextMate- grammar syntax highlighting (oniguruma regex + VSCode .tmLanguage/theme JSON), and a project/scene splash screen. In other words, MathAnimation is a small IDE, not a library — the polar opposite of Manim's "write Python, run, watch the file" loop, and the strongest evidence in this survey that a native, ImGui-based timeline editor over a retained scene is a tractable single-developer build.


Extensibility & API surface

Extensibility is a Luau scripting layer, not a plugin ABI or a scene-description DSL. A ScriptObject runs a user script (authored in the Asset Manager, stored as a project asset) that procedurally generates objects through a C API registered on the Lua state. LuauLayer compiles scripts to bytecode with luau_compile and loads them with luau_load, gated by a ScriptAnalyzer that statically type-checks the source first (analyzer->analyze(...) before compile) — leaning on Luau's gradual type system (LuauLayer.cpp). The exposed surface (extern "C" functions in GlobalApi.h) is deliberately small and SVG-centric:

cpp
int global_createAnimObjectFn(lua_State* L);      // spawn an AnimObject
int global_setAnimObjPosVec3(lua_State* L);        // …position/color/name
int global_svgBeginPath(lua_State* L);             // build an SvgObject procedurally
int global_svgMoveTo(lua_State* L);   int global_svgLineTo(lua_State* L);
int global_svgQuadTo(lua_State* L);   int global_svgCubicTo(lua_State* L);
int global_svgArcTo(lua_State* L);    /* + *Rel relative-command variants */
int global_require(lua_State* L);     int global_loadMathAnimLib(lua_State* L);

So a script's power is exactly "construct SVG paths and animation objects from code" — a scripting escape hatch bolted onto a GUI-first tool, rather than the code-first primary interface Manim exposes. There is no C plugin API, no network/IPC surface, and no stable versioned SDK; the "API" is these Lua globals plus the JSON project schema. For a D reimplementation the lesson is that a typed embeddable scripting VM (Luau here; a D-native or WASM equivalent there) can cover procedural generation without committing to a plugin ABI.


Determinism, caching & performance

MathAnimation caches aggressively and samples deterministically for export, but — being GPU-backed — is not bit-reproducible, so it makes no oracle guarantee.

Content-hash caching at two levels. LaTeX is cached on disk by the MD5 of the equation string (latex/<md5>.svg), compiled once ever (LaTexLayer.cpp). SVG fills are cached in the GPU atlas keyed by a composite hash of md5(path) + svgScale + percentReplacementTransformed, rounded to fixed precision so tiny float wobble doesn't thrash the cache (SvgCache.cpp):

cpp
uint64 hash(const uint8* svgMd5, size_t len, float svgScale, float replacementTransform) {
    int roundedSvgScale = (int)(svgScale * 1000.0f);        // 3 decimal places
    int roundedTransform = (int)(replacementTransform * 100.0f);
    /* combineHash(md5) ⊕ combineHash(scale) ⊕ combineHash(transform) */
}

That the Transform progress is in the key is notable: a morphing object re-rasterises each distinct frame but re-uses the atlas entry whenever the same intermediate recurs. The project's binary SVG format is itself a determinism/compactness decision — paths serialise to absolute-only MoveTo/LineTo/CurveTo/QuadTo commands with coordinates normalised to (int)(val * 1e6) int32s, base64-encoded into the JSON (SvgParser.h):

"Any numbers that follow a command are the coordinate pairs … The normalized value is calculated by doing: (int)(val * 1e6) to truncate the value at 6 decimal places as an integer."

Deterministic frame sampling holds only for export (PlayForwardFixedFrameTime at 60 fps, synchronous rasterisation, Ultra fidelity). But the pixels come from an OpenGL rasteriser plus a CPU plutovg fill, so output is not guaranteed bit-identical across GPUs/drivers the way Manim-community's Cairo path is — MathAnimation is a real-time-preview engine that happens to also write a file, not a regression-tested CPU oracle. Performance levers on show: async PBO readback, a worker-thread encoder over a memory-mapped frame cache, the four-attachment LRU atlas, and an optional Optick profiler build.


Strengths

  • Genuinely single-binary and native. One C++17 executable, every dependency vendored as a submodule and compiled in — no runtime, no interpreter, no external render server. The exact shape a D engine would target.
  • Real-time GUI + audio preview. A dockable ImGui timeline with scrubbing, gizmos, snapping, and waveform sync — the workflow Manim lacks.
  • Pragmatic hybrid renderer. Vendors a CPU vector library (plutovg) for fills → GPU atlas, and writes custom GL only for the animated stroke — a small, buildable rendering surface for one developer.
  • Faithful Manim primitives. Piecewise-Bézier SvgObjects, Create/ Transform/FadeIn animations, LaTeX via dvisvgm, FreeType glyph outlines — the same conceptual model, natively.
  • Two-level content-hash caching (LaTeX-by-MD5, SVG-atlas-by-hash) and a compact binary project format.
  • Embeddable typed scripting (Luau) with static analysis for procedural object generation.

Weaknesses

  • Restrictive custom EULA, no OSI license — the code is readable and compilable for personal use but binary redistribution is forbidden (EULA.txt); a D engine cannot copy code from it, only ideas.
  • Windows-first, effectively unmaintained. master HEAD is 2024-02-03, no tags/releases ever, a single primary author; Linux support is partial.
  • README oversells the state. "mp4" export is really AV1/IVF named .mov; several animations are NOT IMPLEMENTED/BROKEN; the NVENC encoder is a 0-byte stub.
  • Not reproducible. GPU + CPU-fill output is not bit-identical across drivers; no oracle guarantee, no perceptual-diff test harness.
  • Heavy LaTeX dependency. Math objects need a full MiKTeX + dvisvgm install on PATH, shelled out as subprocesses (same friction as Manim).
  • No complex-script shaping (no HarfBuzz), no plugin ABI, and the docking-ImGui codebase carries a lot of editor surface area for one person.

Key design decisions and trade-offs

DecisionRationaleTrade-off
Single C++ binary, all deps vendored as git submodulesSelf-contained native app; no runtime/interpreter; reproducible build~24 submodules compiled from source; heavy first build; Windows-first toolchain
Hybrid renderer: plutovg CPU fills → GPU atlasAvoids writing a GPU fill rasteriser; analytic-coverage AA for freeCPU→GPU upload per uncached shape; not bit-reproducible; two code paths (fill/stroke)
GL miter-tessellated strokes with partial-perimeter drawEnables the animated Create write-on effect nativelyCPU tessellation; join quality is miter-only; separate from the fill path
GUI timeline over a retained sceneReal-time scrub/preview + audio sync — the whole point vs ManimA whole IDE to build/maintain (panels, undo, gizmos, asset mgr) for one developer
Time in integer frames; export at fixed 60 fpsSimple deterministic sampling; clean timeline gridFrame rate effectively hard-coded; sub-frame timing not expressible
LaTeX by shelling out to MiKTeX latex + dvisvgmReuses the real TeX stack; identical output to Manim; MD5 disk cacheRequires a full LaTeX install on PATH; subprocess latency; Windows path assumptions
FreeType glyph outlines (not a bitmap atlas) for scene textResolution-independent, animatable, SvgObject-uniform with shapesNo HarfBuzz shaping; complex scripts unsupported
Linked SVT-AV1 → hand-muxed IVFIn-process encode, no ffmpeg dependency, modern codecOutput is IVF/AV1 mislabelled .mov/.mp4; no broad container/codec choice; NVENC stub
Custom EULA, source-available but no redistributionAuthor keeps control of a personal toolNot open-source; code is reference-only for other projects
Luau scripting escape hatch with static analysisProcedural generation without a plugin ABI; typed, embeddableSmall fixed API surface; scripting is secondary to the GUI, not a primary interface

Sources

  • README.md — positioning ("nearly identical animations to those produced by Manim, except in realtime with a GUI+audio preview"), licensing ("not free for you to distribute any binary copies"), the full feature list (panels, animation types with NOT IMPLEMENTED/BROKEN markers).
  • EULA.txt — the custom (Aseprite-derived) license: "You may not distribute copies of the SOFTWARE PRODUCT to third parties", source may be compiled/modified "for your own personal purpose or to propose a contribution".
  • CMakeLists.txt · Animations/CMakeLists.txt · .gitmodules — C++17/CMake build, single MathAnimations target, the vendored-submodule dependency graph and target_link_libraries list.
  • Animation.hAnimObject/Animation structs, AnimTypeV1/ AnimObjectTypeV1/PlaybackType enums, frame-based timing, applyAnimation.
  • CMath.hEaseType/EaseDirection (Penner easing) + ease(...).
  • Svg.h · Svg.cppSvgObject/Path/Curve geometry, fillWithPluto (CPU fill, fill rules, "Render the SVG in white then color it"), renderOutline2D (GL write-on stroke), Svg::interpolate (Transform alignment).
  • SvgCache.cpp — 4096×4096 four-attachment atlas, LRU eviction, content-hash key, sync-vs-async rasterisation, "Everything is 3D now…".
  • SvgParser.h · SvgParser.cpp — tinyxml2 .svg import; the base64 binary path format ((int)(val * 1e6) normalisation).
  • Renderer.h · Renderer.cpp — the custom GL path API, push/pop style stacks, draw-list batching, generateMiter3D, RgbToYuvShader, jump-flood outline.
  • Fonts.cpp — FreeType outline extraction (FT_LOAD_NO_SCALE, FT_OutlineGlyph, on/off-curve point classification, kerning).
  • LaTexLayer.cpp — MiKTeX latex + dvisvgm subprocess pipeline, standalone preamble, MathTex align* wrap, MD5-keyed SVG disk cache.
  • ExportPanel.cpp · Encoder.cpp — fixed-60 fps PlayForwardFixedFrameTime stepping, GPU RGB→YUV pass, PBO readback, SVT-AV1 C API, hand-written IVF (DKIF/AV01) muxer, .mov default, NVENC TODO; NvidiaEncoder.cpp is a 0-byte stub.
  • GlobalApi.h · LuauLayer.cpp — the Luau scripting surface (global_svg*/global_createAnimObjectFn), luau_compile/luau_load, ScriptAnalyzer static type-checking.
  • External libraries: plutovg, SVT-AV1, Luau, Dear ImGui, FreeType, Manim.