Using wanim
There are three ways to put a scene on a page. Pick the one that fits where your code lives:
| Approach | Use it when |
|---|---|
<wanim-scene> element | You want to drop a scene into any HTML page with no page script |
| iframe with code in the URL | You can only paste an <iframe>, for example in a blog or CMS |
| JavaScript API | You are building a player, an editor, or your own courseware shell |
All three compile and run the Python in the visitor's browser. Only embed code you trust.
Getting the runtime
wanim is published on npm as @wanimjs/wanim. Install it with your usual package
manager:
npm install @wanimjs/wanim
The package has one name and several entry points, each imported by subpath:
| Import | What it gives you |
|---|---|
@wanimjs/wanim | the manim-compatible library: mobjects, scenes, animations, embed helpers |
@wanimjs/wanim/bridge | runPythonOnCanvas, which compiles Python and plays it on a canvas |
@wanimjs/wanim/py2js | the Python-to-JavaScript compiler helpers (Node-safe) |
@wanimjs/wanim/browser/element | the <wanim-scene> custom element |
@wanimjs/wanim/browser/rapydscript | the patched compiler, as a classic script |
In a bundled app (Vite, webpack, esbuild, and similar), import these
subpaths and the bundler picks the browser build for you.
On a plain static page, load the package's browser/ files from your own
server or from a CDN — jsDelivr and JSDMirror both mirror the package. The
bundles reference each other with relative URLs, so keep the folder together,
including its chunks/ subfolder. This documentation site loads the same
files from the JSDMirror CDN.
The compiler also needs numpy-ts, the array library behind wanim's numpy
support. Bundlers find it automatically. On a plain page, load
numpy-ts.browser.js (from the numpy-ts package's dist/ directory) and
add an import-map entry for it, as shown in the next section.
Text is set in open-licensed fonts. By default wanim loads them from a public
CDN. If your audience should not depend on a third-party origin, host the fonts
yourself and call configure_fonts([...]). See For developers
for the font chain.
Web component
The wanim-element.browser.js bundle registers a <wanim-scene> element. For
a plain page, add an import map for numpy-ts, load the element, and write
the scene inside the tag:
<script type="importmap">
{
"imports": {
"numpy-ts": "https://cdn.jsdmirror.com/npm/numpy-ts/dist/numpy-ts.browser.js"
}
}
</script>
<script type="module" src="https://cdn.jsdmirror.com/npm/@wanimjs/wanim/browser/wanim-element.browser.js"></script>
<wanim-scene quality="-qm" autoplay>
<script type="text/x-python">
from manim import *
class Demo(Scene):
def construct(self):
self.play(Create(Circle()))
</script>
</wanim-scene>
The element downloads the compiler and runtime the first time it runs. The
Python can also come from the code property or from a src URL. Inline
source is de-indented automatically, so the indentation of the tag does not
matter.
Attributes
| Attribute | Property | Values and notes |
|---|---|---|
code | .code | Python source. Without it, the element uses the inline <script> or src |
src | — | URL of a .py file to fetch |
quality | .quality | -ql (854×480), -qm (1280×720, default), -qh (1920×1080), -q4k (3840×2160) |
fps | — | target frame rate, default 30. See the note below |
scene | .sceneName | the class to run, when the file defines several |
background | — | CSS colour that overrides the scene's background |
autoplay | — | on by default. autoplay="false" turns it off |
loop | — | run again 250 ms after each finish |
preload-modules | — | space-separated list: mathjax, fonts. Fetches them before the first run |
compiler-src | — | URL of a different rapydscript.js |
| — | .running | read-only. true while a run is in progress |
fps is a target, not a guarantee. The runtime waits for the browser's
next animation frame and then paces logical frames to the target interval. The
real rate can be lower when the display refreshes more slowly than the target,
when a scene's frames take longer to compute, or when the tab is in the
background and the browser pauses animation frames.
Methods
| Method | What it does |
|---|---|
run() | compile and play; returns a promise that resolves with the render statistics |
stop() | abort the current run |
restart() | run again from the beginning |
next() | advance a running interactive presentation to its next page |
previous() | go back one page |
replay() | replay the current page |
next(), previous(), and replay() return false when no presentation is
running.
Events
Events bubble and cross the shadow DOM, so you can listen on document:
| Event | detail | When it fires |
|---|---|---|
wanim-ready | {} | the element is upgraded |
wanim-run | {} | a run starts |
wanim-scene | {name} | a scene class is about to play |
wanim-done | {stats, sceneName} | the run finished |
wanim-error | {error} | fetching, compiling, or rendering failed |
Scripted use is a few lines:
<wanim-scene id="demo" quality="-qm" autoplay="false"></wanim-scene>
<script type="module">
const el = document.getElementById("demo");
el.addEventListener("wanim-done", (e) => console.log(e.detail.stats));
el.addEventListener("wanim-error", (e) => console.error(e.detail.error));
el.code = "from manim import *\nclass Demo(Scene):\n def construct(self):\n self.play(FadeIn(Square()))\n";
await el.run();
</script>
Styling
The element's canvas and status line are exposed as shadow parts:
wanim-scene::part(canvas) { border-radius: 8px; }
wanim-scene::part(status) { font-size: 12px; }
Iframe player: code in the URL fragment
When you can only paste HTML, encode the Python into the URL fragment of a
player page and place an <iframe>:
import { build_embed_hash, build_embed_iframe } from "@wanimjs/wanim";
const hash = await build_embed_hash(pythonSource, { quality: "-qm" });
const snippet = build_embed_iframe(`https://your-host.example/embed.html${hash}`, {
width: 960,
height: 540,
title: "Square to circle",
});
The fragment is compressed with deflate-raw and base64url-encoded. Browsers
do not send fragments to servers, so the player page needs no database, and
the code stays out of request logs. The generated <iframe> is responsive and
sandboxed.
The repository ships demo/embed.html, a ready-made player page. Host it
anywhere you like and point the URL at it.
Controlling the player
The player posts lifecycle messages to its parent and accepts commands. It ignores anything that does not come from its parent window:
| Direction | Message |
|---|---|
| player → host | {source: "wanim-embed", v: 1, type: "ready" | "scene" | "done" | "error", ...} |
| host → player | {type: "wanim:run"} |
| host → player | {type: "wanim:stop"} |
| host → player | {type: "wanim:set-code", code, quality?} |
wanim:set-code replaces the code and re-runs only if the player is already
running. Send wanim:run afterwards if you want an immediate run:
frame.contentWindow.postMessage({type: "wanim:set-code", code: nextScene}, "*");
frame.contentWindow.postMessage({type: "wanim:run"}, "*");
JavaScript API
Use the JavaScript API when you want full control: your own play button, your
own layout, or several canvases on one page. The sequence is: configure
numpy-ts, load the library, register a font chain, load the compiler once,
then call runPythonOnCanvas.
The numpy-ts import map from the previous section is required here too: the
bundles import it by name, and the URLs must match so the page loads a single
numpy-ts instance.
import { configureWasm } from "https://cdn.jsdmirror.com/npm/numpy-ts/dist/numpy-ts.browser.js";
configureWasm({ maxMemory: 1024 * 1024 * 1024 });
const wanim = await import("https://cdn.jsdmirror.com/npm/@wanimjs/wanim/browser/wanim.browser.js");
const bridge = await import("https://cdn.jsdmirror.com/npm/@wanimjs/wanim/browser/bridge.browser.js");
wanim.configure_fonts(wanim.DEFAULT_FONT_URLS);
// The patched compiler is a classic script. Inject it once per page.
await new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = "https://cdn.jsdmirror.com/npm/@wanimjs/wanim/browser/rapydscript.js";
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
const { controller, sceneNames } = await bridge.runPythonOnCanvas(pythonSource, canvas, {
quality: "-qm", // -ql | -qm | -qh | -q4k
realtime: true, // draw while the scene plays
fps: 30,
signal: abortController.signal,
onScene: (name) => console.log("now playing", name),
onController: (c) => { /* keep this to drive navigation from outside */ },
});
runPythonOnCanvas plays every Scene subclass in the source, in order, as
one presentation. Pass sceneName to play just one. The returned controller
has next(), previous(), replay(), and restart().
The main options are:
| Option | Default | Meaning |
|---|---|---|
quality | -qm | resolution and frame rate preset |
realtime | true | draw while the scene plays, yielding to the browser between frames |
fps | 30 | target logical frame rate |
background | scene's own | CSS colour override |
sceneName | all scenes | play only this class |
signal | — | an AbortSignal that stops the run |
preload | "auto" | "auto" detects MathJax and font needs; "none" skips them |
All three approaches compile and execute Python in the visitor's browser. Only
embed code you trust, and treat any src URL the same way you would treat a
script.