跳到主要内容

使用 wanim

有三种方式把场景放进页面。根据代码所在的位置选择:

方式适用场景
<wanim-scene> 元素想在任意 HTML 页面放入场景,不需要写页面脚本
URL 中带代码的 iframe只能粘贴 <iframe>,例如博客或 CMS
JavaScript API在做自己的播放器、编辑器或课件外壳

三种方式都会在访问者的浏览器中编译并运行 Python。只嵌入你信任的代码。

获取运行时​

wanim 已发布到 npm,包名为 @wanimjs/wanim。用你常用的包管理器安装:

npm install @wanimjs/wanim

这个包只有一个名字,但提供多个入口,按子路径引入:

引入路径提供的内容
@wanimjs/wanim兼容 manim 的核心库:图形对象、场景、动画、嵌入辅助函数
@wanimjs/wanim/bridgerunPythonOnCanvas:编译 Python 并在 canvas 上播放
@wanimjs/wanim/py2jsPython 到 JavaScript 的编译辅助函数(可在 Node 中使用)
@wanimjs/wanim/browser/element<wanim-scene> 自定义元素
@wanimjs/wanim/browser/rapydscript打过补丁的编译器,作为经典脚本加载

在打包工具项目中(Vite、webpack、esbuild 等),直接 import 这些子路径即可,打包工具会自动选择浏览器版本。

在普通静态页面中,从你自己的服务器或 CDN 加载包里的 browser/ 文件——jsDelivr 与 JSDMirror 都镜像了这个包。各个 bundle 之间用相对 URL 互相引用,所以请保持整个目录完整(包括其中的 chunks/ 子目录)。本文档站点就是从 JSDMirror CDN 加载这些文件的。

编译器还需要 numpy-ts,它是 wanim 支持 numpy 的数组库。打包工具会自动找到它。在普通页面中, 从 numpy-ts 包的 dist/ 目录加载 numpy-ts.browser.js,并为它添加 import map 条目, 见下一节的示例。

文字使用开源字体。默认情况下,wanim 从公共 CDN 加载字体。如果你的读者不应依赖第三方域名, 可以自行托管字体,并调用 configure_fonts([...])。字体回退链见 开发者文档。

Web Component​

wanim-element.browser.js 会注册 <wanim-scene> 元素。在普通页面中,先添加 numpy-ts 的 import map,再加载元素,最后把场景写在标签里:

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

元素在第一次运行时才下载编译器和运行时。Python 代码也可以通过 code 属性或 src 地址提供。 内联源码会自动去除公共缩进,所以标签本身的缩进不影响运行。

属性​

属性对应属性(property)取值与说明
code.codePython 源码。未设置时,使用内联 <script> 或 src
src—要获取的 .py 文件地址
quality.quality-ql(854×480)、-qm(1280×720,默认)、-qh(1920×1080)、-q4k(3840×2160)
fps—目标帧率,默认 30。见下方说明
scene.sceneName文件中定义了多个场景类时,指定要运行的类
background—覆盖场景背景色的 CSS 颜色
autoplay—默认开启。设为 autoplay="false" 可关闭
loop—每次播放结束后 250 毫秒再次运行
preload-modules—以空格分隔的列表:mathjax、fonts。在首次运行前预先加载
compiler-src—替换 rapydscript.js 的地址
—.running只读。运行过程中为 true
备注

fps 是目标值,不是保证值。运行时会等待浏览器的下一个动画帧,再按目标间隔节流逻辑帧。 实际帧率可能更低:显示器刷新率低于目标值、场景每帧计算耗时较长,或者标签页处于后台时浏览器 会暂停动画帧。

方法​

方法作用
run()编译并播放;返回一个 Promise,完成时给出渲染统计信息
stop()中止当前运行
restart()从头再运行一次
next()让正在运行的交互演示进入下一页
previous()返回上一页
replay()重播当前页

当没有正在运行的演示时,next()、previous() 和 replay() 返回 false。

事件​

事件会冒泡,并穿过 Shadow DOM,因此可以在 document 上监听:

事件detail触发时机
wanim-ready{}元素完成升级
wanim-run{}开始运行
wanim-scene{name}即将播放某个场景类
wanim-done{stats, sceneName}运行结束
wanim-error{error}获取、编译或渲染失败

通过脚本使用的写法如下:

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

样式​

元素的画布和状态栏暴露为 Shadow DOM 的 part,可以从外部设置样式:

wanim-scene::part(canvas) { border-radius: 8px; }
wanim-scene::part(status) { font-size: 12px; }

iframe 播放器:代码放在 URL 片段中​

如果只能粘贴 HTML,可以把 Python 编码进播放器页面 URL 的片段(# 之后的部分),再放置一个 <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: "正方形变圆形",
});

片段会先用 deflate-raw 压缩,再进行 base64url 编码。浏览器不会把 URL 片段发送给服务器, 因此播放器页面不需要数据库,代码也不会出现在请求日志中。生成的 <iframe> 会自适应宽度,并且是沙箱化的。

仓库中提供了现成的播放器页面 demo/embed.html。把它部署到任意位置,再把 URL 指向它即可。

控制播放器​

播放器会向父页面发送生命周期消息,并接受指令。它只处理来自父窗口的消息:

方向消息
播放器 → 宿主页面{source: "wanim-embed", v: 1, type: "ready" | "scene" | "done" | "error", ...}
宿主页面 → 播放器{type: "wanim:run"}
宿主页面 → 播放器{type: "wanim:stop"}
宿主页面 → 播放器{type: "wanim:set-code", code, quality?}

wanim:set-code 会替换代码;只有当播放器已在运行时才会重新运行。如果希望立即运行,随后再发送 wanim:run:

frame.contentWindow.postMessage({type: "wanim:set-code", code: nextScene}, "*");
frame.contentWindow.postMessage({type: "wanim:run"}, "*");

JavaScript API​

需要完全控制时(自己的播放按钮、自己的布局,或同一页面中的多个画布)使用 JavaScript API。 流程是:配置 numpy-ts、加载库、注册字体回退链、加载一次编译器,然后调用 runPythonOnCanvas。

上一节那条 numpy-ts 的 import map 在这里同样必需:各个 bundle 会按名字导入它,并且 URL 必须一致,页面才会只加载一个 numpy-ts 实例。

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);

// 打过补丁的编译器是经典脚本,每个页面只注入一次。
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, // 边播放边绘制
fps: 30,
signal: abortController.signal,
onScene: (name) => console.log("正在播放", name),
onController: (c) => { /* 保存引用,以便从外部控制翻页 */ },
});

runPythonOnCanvas 会按顺序把源码中的每个 Scene 子类作为一个演示的一页来播放。只想播放其中一个时, 传入 sceneName。返回的 controller 提供 next()、previous()、replay() 和 restart()。

主要选项如下:

选项默认值含义
quality-qm分辨率与帧率预设
realtimetrue播放时实时绘制,并在帧之间让出浏览器
fps30目标逻辑帧率
background场景自身的背景覆盖背景色的 CSS 颜色
sceneName所有场景只播放该类
signal—用于中止运行的 AbortSignal
preload"auto""auto" 会自动检测是否需要 MathJax 和字体;"none" 跳过预加载
警告

三种方式都会在访问者的浏览器中编译并执行 Python。只嵌入你信任的代码,并且像对待任何脚本一样对待 src 地址。