Porting Manim to TypeScript with Coding Agents

Coding Agents TypeScript Open Source
By Narek Maloyan ·

What this post is. A look at how manim-web was built, read from its git history, pull requests, docs, and public stats. Commit counts come from the git author field and co-author trailers on main. Stars, downloads, and the Hacker News score were checked through the public APIs on 10 October 2026.


Short version

The goal is on the projects page: Manim gives you a video file, and manim-web puts the same scenes live on a web page, where the reader can touch them.

What a Manim port has to cover

Manim has a small core and a very large surface. The core is a scene, a tree of mobjects, and animations that change mobjects over time. A VMobject is a list of cubic Bezier control points: anchor, handle, handle, anchor. Almost everything on screen is a VMobject: circles, arrows, axes, glyphs of text, and LaTeX formulas. The surface is the hundreds of classes built on top of that core.

Python Manim renders a scene to a video file. manim-web keeps the same object model but draws every frame in the browser. The table shows how the main parts map.

Python Manimmanim-web
class MyScene(Scene) with construct()An async function that takes a Scene
self.play(...), self.wait()await scene.play(...), await scene.wait(), driven by requestAnimationFrame
Mobject treeMobjects backed by Three.js objects
VMobject strokesThree.js Line2, a mesh stroke with miter joins, or a signed distance field shader for cubic Bezier curves
VMobject fillsTriangulation with earcut
Boolean operations on shapespolygon-clipping
LaTeX through a TeX installMathJax SVG converted to VMobject paths, or KaTeX rendered to a texture
Text glyphsopentype.js outlines
3D scenes and cameraThree.js lights, meshes, and orbit controls
Video outputLive canvas, plus GIF and video export

The repo also ships tools/py2ts.cjs, a regex-based converter from Python scenes to TypeScript. Its docs say it knows about 170 mobject classes, more than 45 animations, and more than 80 method mappings. This is the example from the docs:

// Python: self.play(Transform(square, circle))
export async function squareToCircle(scene: Scene) {
  const square = new Square({ color: BLUE });
  const circle = new Circle({ fillOpacity: 0.8, fillColor: PINK });
  await scene.play(new Create(square));
  await scene.play(new Transform(square, circle));
  await scene.wait();
}

Timeline

How the agent work was split

The port phase ran in Gas Town, an open-source workspace for running Claude Code agents. Its README describes the Mayor as the primary AI coordinator, a Claude Code instance with context about the workspace. Three facts from Narek's local Gas Town workspace tie it to manim-web:

So the port phase had one agent at a time, not a fleet. More agents came later, in the PR phase described below.

Commit authors give a partial picture. The agent wrote under its own author name, mayor, with Narek's email. GitHub merges both names into one contributor, which is why the contributors page shows 394 commits for maloyan: 293 under Narek's name plus 101 under mayor.

Author on mainUp to 19 Feb 2026All time (616)
mayor (agent)101101
Narek Maloyan55293 (89 of them merge commits)
Dependabot13138
Outside contributors084

The work after 19 February moved to pull requests. 147 of Narek's own PRs, opened between 1 February and 16 July, were merged. 27 of them carry the Claude Code footer. 16 of his PRs say that a second model, Codex, reviewed the plan, the diff, or both. A typical note from PR #385: the plan was posted on the issue, reviewed with Codex, and Codex flagged three issues, all fixed in the branch. This is a pattern of one agent to write and a different model to review.

The record has one blind spot. The 293 commits under Narek's own name carry only 4 AI co-author trailers, and the PR footers mark only 27 PRs. Neither field shows how much of the rest an agent wrote. This post counts only what the author field, the trailers, and the PR bodies show.

The hard parts

Strokes and fills on WebGL

WebGL has no built-in thick curved line, so the code has three stroke paths. Line2 covers the simple case. A mesh stroke with miter joins gives sharp corners on closed paths. A shader draws each cubic segment as a screen-aligned quad and finds the distance to the curve per pixel with Newton steps. Fills go through earcut. Boolean operations first had their own implementation and were rewritten on top of polygon-clipping on 31 January, the same day.

LaTeX

LaTeX work runs through the history from February to May. The first version used KaTeX and drew formulas as images. That works for display but not for path animations such as Create and Transform, which need real paths to draw a stroke or morph one formula into another. On 14 February the agent added MathTexSVG: MathJax renders SVG, and a parser turns it into VMobject paths.

The details then took months. MathJax draws subscripts and superscripts with a scale() transform, about 70.7% of full size. The first parser read only translate(), so indices rendered at full size and overlapped. PR #28 fixed it on 16 February. Rendering is also async, so Scene.play() had to wait for pending renders before it starts an animation. On 12 April the vector path became the default for MathTex and Tex. Later PRs moved to MathJax 4 (#300) and kept one MathJax instance across bundle chunks (#412).

Text and the Write animation

Manim's Write draws text as if a pen traces each letter. A font gives outlines, not pen paths. The port gets the outline from opentype.js, rasterizes it to a grid, thins it to a one-pixel skeleton with the Zhang-Suen algorithm, traces the skeleton into chains, and fits cubic Beziers to them.

Transforms

Transform morphs one shape into another, so both shapes need the same number of points in matching order. Early commits added Bezier-aware point alignment and a glyph-based morph for text to text. TransformMatching pairs parts with the Hungarian algorithm. A June fix from an outside contributor corrected corner tangents when cubic chains are resampled (#446).

Matching Python's coordinate rules

Small differences in frames and centers break many scenes at once. v0.3.23 shipped a breaking change: getCenter now matches Python Manim, and moveTo, rotate, and scale move their anchor into the parent frame.

Where the agents did well

Where a human was needed

Results

MetricValue
GitHub stars and forks485 and 31
npm downloads, 9 Oct 2025 to 8 Oct 202682,553
Show HN, 25 Feb 2026140 points, 25 comments
GitHub releases27, from v0.1.0 (11 Feb) to v0.3.24 (13 Jul)
Commits on main616
Source, without testsabout 80,000 lines of TypeScript in 245 files
Tests6,170 passing (PR #541), about 69,000 lines
Example scenes56 TypeScript files

Lessons from the record

Sources



Narek Maloyan holds a PhD in Computer Science from Lomonosov Moscow State University and works as an AI Research Engineer at Zencoder. His research focuses on AI safety, LLM security, and adversarial machine learning. Learn more