@agarwal29796/manim-js
v0.3.1
Published
Manim.js — a Manim-inspired, framework-agnostic animation engine for the web. Author scenes in code; render anywhere via a tiny Renderer interface.
Downloads
270
Maintainers
Readme
Manim.js
Manim-inspired, framework-agnostic animation engine for the web. Author scenes in code, compile them to a deterministic timeline, and render anywhere through a tiny
Rendererinterface.
Independent project inspired by Manim; not affiliated with or endorsed by the Manim Community.
Why
- Deterministic. A scene compiles to a
Timelinewhosesample(t)is a pure function of time. Scrubbing, seeking and frame-accurate export all just work — no live state to unwind. - Renderer-agnostic. The engine (
core/) has zero dependencies and never touches a canvas. Pixels are someone else's job, behind a 2-methodRendererinterface. The package ships a Canvas2D renderer; embed your own to draw through an existing pipeline. - Real morphing. Shapes are point sets, so
transform(circle, square)interpolates any pair of shapes by resampling to a common point count.
Install
npm i @agarwal29796/manim-jsQuick start
import { Scene, circle, square, text, create, write, transform } from "@agarwal29796/manim-js";
import { CanvasRenderer, Player } from "@agarwal29796/manim-js/canvas";
const s = new Scene({ width: 1280, height: 720 });
const title = text("hello", { y: -200, size: 80 });
const c = circle({ r: 120, stroke: "#5B5BD6" });
s.play(write(title));
s.play(create(c));
s.play(transform(c, square({ size: 220, stroke: "#FF6B5B" })));
const timeline = s.compile();
const renderer = new CanvasRenderer(canvas.getContext("2d")!);
new Player(timeline, renderer, 1280, 720).play();Authoring vocabulary
Scene.play(...anims, { run, ease }), Scene.wait(s), Scene.seek(t), Scene.add(...), Scene.sound(...).
- Shapes:
circle,square,rect(rounded viaradius),polygon,line,functionGraph,numberLine,axes,text,texture - Animations:
create,write,fadeIn,fadeOut,transform,shift,scaleBy,rotate,indicate,moveCamera,zoomCamera - Easing:
linear,easeInOut,easeOut,easeIn,easeOutBack,thereAndBack,spring({ stiffness, damping, mass }) - Style:
stroke,fill,opacity,strokeWidth, plusgradient(linearGradient/radialGradient),shadow(softShadow),blur,blend,dash; textweight/italic/letterSpacing/lineHeight
Two kinds of node, one interface
Everything is a node behind one interface, so fadeIn/shift/scale/transform operate on all of them identically:
- Native (vector) — shapes, text, LaTeX: geometry sampled at
t, drawn by the renderer. - Texture (raster) —
texture(source): a rectangle whose pixels come from an outside renderer (image, video, rasterised HTML/PDF, webcam). ATextureSourcegivesframeAt(t)for live preview and an asyncprepare(t)so seekable sources (video, GIF) export frame-exact.
Scene IR — declare the objects, script the time
The serializable, versioned source of truth: id'd declarative nodes (what exists) + a scripted ops timeline (what happens). Op times can anchor to named beats that re-time the whole scene when moved.
import { compileIR } from "@agarwal29796/manim-js";
const { timeline } = compileIR({
version: 1, width: 1280, height: 720,
beats: [{ name: "reveal", at: 1.5 }],
nodes: [
{ id: "tri", type: "polygon", props: { points: [[-150,120],[150,120],[150,-80]], fill: "#5B5BD6" } },
{ id: "eq", type: "text", props: { text: "a²+b²=c²", y: 240 } },
],
ops: [
{ anim: "create", target: "tri", at: 0, run: 1 },
{ anim: "write", target: "eq", at: { beat: "reveal" }, run: 1 },
],
});The whiteboard round-trips nodes (drag tri → patch its props on that id); the AI writes ops; both edit one artifact. serializeIR/parseIR/validateIR and a SceneBuilder come along.
The seam (for custom renderers)
interface Renderer {
clear(width: number, height: number): void;
draw(op: DrawOp): void; // DrawOp = PolyOp | TextOp
}That is the entire public contract between engine and pixels. Implement it and the engine runs unchanged. (This is how it renders natively inside other apps.)
Layout
src/core/ engine — mobjects, animations, scene/timeline, IR (zero deps)
src/canvas/ reference Canvas2D renderer + rAF Player
studio/ browser playground (npm run studio)Develop
npm install
npm run build # tsup -> dist (ESM + CJS + .d.ts)
npm run studio # bundle + serve the live-coding playground at :8123
npm test # vitest
npm run check:exports # publint + are-the-types-wrongRoadmap
- [x] LaTeX
Write—@agarwal29796/manim-js/tex(MathJax → SVG paths).await initTex(), thentex("E=mc^2"). - [x]
axes()/ coordinate plane (withc2p/p2c/plot),brace(),vector(),dot() - [x] Easing
lag/stagger inplay(play(..., { lag })andstagger([...])) - [x] Camera moves —
moveCamera()/zoomCamera() - [x] Frame-accurate MP4 export off the timeline —
@agarwal29796/manim-js/export(exportMP4, WebCodecs) - [x] Rich style — gradients, shadows, blur, blend, rounded corners, typography, alpha colours
- [x] Spring easing (
spring()) - [x] Texture (raster) nodes behind the same interface (
texture()), with exact-seek export - [x] Serializable, versioned scene IR (
compileIR) with re-anchorable beats - [x] Audio as a first-class timeline citizen (
Scene.sound()), muxed on export - [x] Motion blur / temporal AA + any-resolution export (
subFrames,scale) - [ ] More mobjects (matrices, tables), 3D
Subpath entry points
| Import | What | Extra dep |
|---|---|---|
| @agarwal29796/manim-js | core engine (scenes, mobjects, animations) | none |
| @agarwal29796/manim-js/canvas | reference Canvas2D renderer + Player | none |
| @agarwal29796/manim-js/tex | initTex, tex, texGroup (animated LaTeX) | mathjax-full |
| @agarwal29796/manim-js/export | exportMP4, exportFrames (MP4 off the timeline) | mp4-muxer |
The two heavy deps are optional peers — only installed if you use those subpaths; the core stays dependency-free.
License
Apache-2.0 © Archit Agarwal
