Skip to main content

Using wanim

There are three ways to put a scene on a page. Pick the one that fits where your code lives:

ApproachUse it when
<wanim-scene> elementYou want to drop a scene into any HTML page with no page script
iframe with code in the URLYou can only paste an <iframe>, for example in a blog or CMS
JavaScript APIYou 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:

ImportWhat it gives you
@wanimjs/wanimthe manim-compatible library: mobjects, scenes, animations, embed helpers
@wanimjs/wanim/bridgerunPythonOnCanvas, which compiles Python and plays it on a canvas
@wanimjs/wanim/py2jsthe Python-to-JavaScript compiler helpers (Node-safe)
@wanimjs/wanim/browser/elementthe <wanim-scene> custom element
@wanimjs/wanim/browser/rapydscriptthe 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​

AttributePropertyValues and notes
code.codePython 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.sceneNamethe 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
—.runningread-only. true while a run is in progress
note

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​

MethodWhat 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:

EventdetailWhen 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:

DirectionMessage
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:

OptionDefaultMeaning
quality-qmresolution and frame rate preset
realtimetruedraw while the scene plays, yielding to the browser between frames
fps30target logical frame rate
backgroundscene's ownCSS colour override
sceneNameall scenesplay only this class
signal—an AbortSignal that stops the run
preload"auto""auto" detects MathJax and font needs; "none" skips them
caution

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.