放映插件
放映插件在放映时运行。它可以跟着当前页、控制翻页、读取这一页上的形状,并加上面板、工具栏按钮或一层遮罩。
官方放映插件随编辑器提供,id 以 official: 开头。不登录也能用。白板默认开着。
| 插件 | id | 作用 |
|---|---|---|
| 白板 | official:whiteboard | 画笔、荧光笔、橡皮 |
| 激光笔 | official:laser-pointer | 跟着指针的红点 |
| 大纲 | official:outline | 按标题跳转 |
| 演讲者备注 | official:notes-panel | 正在放的这一页的备注 |
| 缩略图 | official:thumbnail-bar | 一条用来跳页的缩略图 |
| 截图 | official:screenshot | 把当前页存成 PNG |
这里的演讲者备注只在放映时出现。编辑器里另有一个同名的官方插件。
导入插件是你加到账号里的文件。要先登录。每条导入插件只属于这个账号,而且不能改成公开。导入之后默认不勾选。到 放映 → 插件 里勾上才会在放映时加载。
导入… 选择一个 .js 文件。删除 会从账号里去掉它。同一个 id 再导入一次会覆盖原来的文件。

写一个
文件里调用一次 definePlaybackPlugin。编辑器加载文件时,defineEditorPlugin 和 definePlaybackPlugin 已经在作用域里,不用自己 import,也不要再调用 PPTPlayer.definePlugin。导出的 HTML 会把 definePlaybackPlugin 接到播放器上。
id 以字母开头,后面可以是字母、数字、.、_、-,最长 64 个字符。不能含 :。文件最长 96 × 1024 个字符。name 保留到 80 个字符,description 保留到 240 个字符。
definePlaybackPlugin({
id: "acme.hello",
name: "你好",
description: "放映时显示文稿标题。",
version: "1.0.0",
activate(ctx) {
const panel = ctx.ui.addPanel({
title: "你好",
render(body) {
body.textContent = ctx.presentation.title || "未命名";
},
});
const off = ctx.events.on("slidechange", (ev) => {
ctx.log(ev.slide.name);
});
return () => {
off();
panel.remove();
};
},
});activate 可以是 async。返回一个函数,用来卸掉监听、计时器和界面。如果写了 deactivate(),它在这个函数之后调用,没有参数。
插件在页面里运行,权限和官方插件一样。
勾选了的放映插件,在账号允许把插件写入导出时,会打进 HTML / HTML zip。编辑插件不会。见 导出。
插件对象
| 字段 | 类型 | 必填 | 含义 |
|---|---|---|---|
id | string | 是 | 稳定 id。字母开头,随后是字母、数字、.、_、-。不含 :。最长 64 个字符 |
name | string | 否 | 插件列表里的名字。缺省用 id。截到 80 个字符 |
description | string | 否 | 名字下面的一行说明。截到 240 个字符 |
version | string | 否 | 你自己的版本号 |
activate | (ctx) => cleanup | void | Promise<cleanup | void> | 是 | 放映开始且这个插件被勾选时调用 |
deactivate | () => void | 否 | 清理函数之后调用,放映结束时会走到 |
activate(ctx)
| 字段 | 用途 |
|---|---|
ctx.presentation | 页列表、尺寸、标题、备注。放映开始时的一份快照 |
ctx.player | 翻页,以及推进这一页的动画 |
ctx.events | slidechange、stepchange、complete、destroy |
ctx.ui | 面板、工具栏按钮、遮罩 |
ctx.scene | 当前页上的形状 |
ctx.thumbnail | 任意一页的矢量缩略图 |
ctx.contextMenu | 放映右键菜单上的项 |
ctx.live | 在直播放映房间里发给其他人 |
ctx.shared | 这一场放映里,所有放映插件共用的一个对象 |
ctx.locale | "en" 或 "zh-CN" |
ctx.log(...args) | console.log,前缀是 [plugin:<id>] |
到处都会碰到两套下标:
- 可见下标是观众正在看的那一页,从
0起。隐藏页会被跳过。ctx.player.goto和slidechange.visibleIndex用的是这一套。 - 绝对下标是
ctx.presentation.slides里的位置,隐藏页也占一位。ctx.presentation.visibleOrder[可见下标]就是绝对下标。
activate 常常发生在第一次 slidechange 之后。读 ctx.player.current(),或者调用 ctx.events.emitCurrentSlide()。
ctx.presentation
| 字段 | 类型 | 含义 |
|---|---|---|
id | string | 文稿 id |
size | { width: number, height: number } | 幻灯片尺寸,像素 |
title | string,可缺 | 文档标题 |
slides | SlideInfo[] | 全部页,按文件顺序 |
visibleOrder | number[] | 放映顺序下的绝对下标 |
meta | object,可缺 | 作者、关键字等文档属性 |
SlideInfo:
| 字段 | 类型 | 含义 |
|---|---|---|
id | string | 页 id |
name | string | 页名 |
index | number | 绝对下标 |
visibleIndex | number | 在放映序列中的位置。隐藏页为 -1 |
show | boolean | 隐藏页为 false |
hasAnimations | boolean | 这一页有可以往下推进的动画步 |
totalAnimationSteps | number | 步数。没有计时时为 0 |
notes | string,可缺 | 演讲者备注 |
transition | { kind: string, duration: number },可缺 | 切换名称,以及时长(秒) |
ctx.player
| 函数 | 参数 | 返回 | 作用 |
|---|---|---|---|
next() | — | void | 推进一个动画步。这一页没有剩余步时翻到下一页。已经是最后一页时触发 complete |
prev() | — | void | 回到上一张可见页。不会把动画步倒退 |
goto(visibleIndex) | visibleIndex: number | void | 跳到这张可见页,从 0 起 |
gotoById(slideId) | slideId: string | boolean | 按页 id 跳。找不到返回 false |
current() | — | number | 当前可见下标 |
total() | — | number | 可见页数 |
nextStep() | — | boolean | 只推进一个单击步,不翻页。这一页没有剩余步时返回 false |
triggerShape(shapeId) | shapeId: string | boolean | 播放这个形状的单击动画。shapeId 是 ShapeInfo.id。没有可播放的动画时返回 false |
step() | — | { totalSteps: number, currentStep: number } | 当前页的动画进度 |
freezeActive() | — | void | 把正在播的步全部停在末态 |
setNavigationLocked(locked) | locked: boolean | void | true 时,单击翻页、键盘翻页、点形状触发动画和超链接都停用。next() 和 goto() 仍然可用 |
isNavigationLocked() | — | boolean | 导航是否锁着 |
如果锁过导航,在清理函数里解开。
ctx.events
| 函数 | 参数 | 返回 |
|---|---|---|
on(event, cb) | event 是下表里的名字。cb 收到该事件的数据 | () => void,调用即取消订阅 |
off(event, cb) | 和 on 时相同的 event 与 cb | void |
emitCurrentSlide() | — | void。再为屏幕上的这一页发一次 slidechange |
| 事件 | 数据 | 何时 |
|---|---|---|
slidechange | { visibleIndex, absoluteIndex, total, slide } | 落到某一页,包括初次加载、goto、next 跨页、超链接。total 是可见页数。slide 是这一页的 SlideInfo |
stepchange | { visibleIndex, absoluteIndex, currentStep, totalSteps, frozen? } | 动画步前进或后退。frozen 为 true 表示这一步被直接停在末态 |
complete | 无 | 放映结束:最后一页之后再推进、结束放映,或点了结束放映的超链接 |
destroy | 无 | 播放器要关掉。清理函数在这之后运行 |
ctx.scene
当前页上的形状。坐标是幻灯片像素,不是屏幕像素。翻页之后,这些函数指向新的一页。
| 函数 | 参数 | 返回 | 作用 |
|---|---|---|---|
shapes() | — | ShapeInfo[] | 顶层形状,从底到顶 |
byName(name) | name: string | ShapeInfo | null | 名字完全相同的第一个形状 |
allByName(name) | name: string | ShapeInfo[] | 名字完全相同的全部形状 |
byId(id) | id: string | ShapeInfo | null | 这个渲染 id 对应的形状 |
hitTest(x, y) | x: number, y: number | ShapeInfo | null | 这个幻灯片坐标上最顶层的可见形状 |
toSlideCoords(clientX, clientY) | clientX: number, clientY: number | { x, y } | null | 屏幕坐标换成幻灯片坐标。舞台还没有尺寸时返回 null |
setVisible(id, visible) | id: string, visible: boolean | boolean | 显示或隐藏这个形状。找不到 id 返回 false |
setOpacity(id, opacity) | id: string, opacity: number | boolean | 透明度,0 到 1。找不到 id 返回 false |
triggerShape(id) | id: string | boolean | 与 ctx.player.triggerShape 相同 |
ShapeInfo:
| 字段 | 类型 | 含义 |
|---|---|---|
id | string | 渲染 id,例如 sp_12。传给 triggerShape |
name | string | 选择窗格里的名字 |
kind | string | text、image、rect、ellipse、line、group、chart,以及其他形状种类 |
bbox | { x, y, w, h } | 幻灯片像素下的包围盒 |
visible | boolean | 当前是否显示 |
text | string,可缺 | 纯文本。文字形状,以及里面有字的组合才有 |
hasClickAnimation | boolean | triggerShape 能不能播放它的单击动画 |
先在选择窗格里给要用的形状起名,再用 byName 找。
ctx.ui
凡是往界面上加东西的调用,都返回 { remove(): void }。
| 函数 | 参数 | 返回 | 作用 |
|---|---|---|---|
addPanel(opts) | 见下表 | { remove() } | 停靠栏里一块可折叠面板 |
addToolbarButton(opts) | 见下表 | { remove() } | 放映工具栏上的按钮 |
addOverlay(opts) | 见下表 | { remove() } | 叠在幻灯片上的 HTML,用幻灯片坐标 |
mount(node) | node: HTMLElement | { remove() } | 把 node 加到播放器根节点上 |
setDockOpen(open) | open: boolean | void | 打开或关掉停靠栏 |
toggleDock() | — | void | 切换停靠栏 |
isDockOpen() | — | boolean | 停靠栏是否开着 |
rootElement() | — | HTMLElement | 播放器根节点 |
stageElement() | — | HTMLElement | 幻灯片舞台 |
addPanel:
| 字段 | 类型 | 必填 | 含义 |
|---|---|---|---|
title | string | 是 | 面板标题 |
render | (body: HTMLElement) => void | 是 | 创建时调用一次,用来填充内容 |
id | string | 否 | 面板 id |
initialOpen | boolean | 否 | 默认展开。设成 false 则一开始收起 |
order | number | 否 | 数字越小越靠上。默认 100 |
addToolbarButton:
| 字段 | 类型 | 必填 | 含义 |
|---|---|---|---|
label | string | 是 | 按钮文字 |
onClick | (e: MouseEvent) => void | 是 | 点击回调 |
tooltip | string | 否 | 悬停文字 |
position | "start" | "end" | 否 | 工具栏左侧或右侧 |
addOverlay:
| 字段 | 类型 | 必填 | 含义 |
|---|---|---|---|
node | HTMLElement | 是 | 要显示的元素 |
bbox | { x, y, w, h } | 否 | 幻灯片像素。不写则铺满这一页 |
zIndex | number | 否 | 叠放顺序 |
ctx.contextMenu
| 函数 | 参数 | 返回 | 作用 |
|---|---|---|---|
addItem(item) | 字段见下表 | { remove() } | 加一行菜单 |
open(x, y) | x: number, y: number,屏幕像素 | void | 在这个位置打开菜单 |
close() | — | void | 关掉菜单 |
addItem 的字段:
| 字段 | 类型 | 含义 |
|---|---|---|
id | string,可缺 | 之后移除这一行时用 |
kind | "item" | "checkbox" | "separator" | 默认 "item" |
label | string,可缺 | 这一行的文字。分隔线可以不写 |
shortcut | string,可缺 | 显示在右侧,放映时也会响应这个键。不带修饰键。一个字符,或者 ← → ↑ ↓ Esc Space Home End Enter F1 PageUp PageDown |
order | number,可缺 | 数字越小越靠上。默认 100 |
group | string,可缺 | 不同分组之间自动加一条分隔线 |
checked | boolean 或 () => boolean | 勾选状态。函数会在每次打开菜单时再算一次 |
disabled | boolean 或 () => boolean | 函数会在每次打开菜单时再算一次 |
onClick | (e?: MouseEvent) => void | e 是这次右键,带 clientX 和 clientY |
ctx.thumbnail
target 可以是可见下标(number)、{ visibleIndex }、{ absoluteIndex } 或 { id }。
| 函数 | 参数 | 返回 | 作用 |
|---|---|---|---|
mount(host, target, opts?) | host: HTMLElement。opts.transparent?: boolean | { element: SVGSVGElement, destroy() } | 把缩略图画进 host。host 自己要有固定尺寸。调用 destroy() 卸掉 |
toSvgString(target, opts?) | 同样的 opts | string | SVG 文本 |
toSvgBlob(target, opts?) | 同样的 opts | Blob | image/svg+xml |
toDataUrl(target, opts?) | 同样的 opts | string | SVG data URL |
clearCache() | — | void | 清掉缩略图缓存 |
byVisibleIndex、byAbsoluteIndex、byId、dataUrlByVisibleIndex 还在,返回 Promise。新插件用 mount 或 toSvgBlob。
ctx.live
这场放映不是直播放映时,这条通道是空的。那时 send 什么也不做,attached() 为 false。
| 函数 | 参数 | 返回 | 作用 |
|---|---|---|---|
attached() | — | boolean | 这个浏览器是主持人,或已被允许进入的观众时为 true |
send(data, opts?) | data: unknown。opts.snap?: boolean | void | 把 data 发给房间里的其他人。snap: true 会留下最近一份,给后加入的人 |
on(fn) | fn: (data, meta: { from: string }) => void | () => void 取消订阅 | 其他人发来一条消息 |
onAttach(fn) | fn: (attached: boolean) => void | () => void 取消订阅 | 直播通道接通或断开。接通时用 snap: true 把当前状态发出去 |
一场直播放映里,翻页、动画步和已经嵌入的音视频由主持页同步。ctx.live 只传这个插件自己的消息。主持人和已准入的观众都能发、都能收。
data 用 JSON 写出后不超过 65536 个字符,更长的会被丢掉。snap: true 只留下这个插件最近的一份。后加入的人接通时先收到这一份。通道接通时,在 onAttach 里把当前状态再发一次。
setVisible 和 setOpacity 发生在调用它们的那一台浏览器上。房间里其他人要看到同一结果,就在 on 里再调一次。
白板把每一页的笔迹收成一份快照发出去,并带上 snap: true。激光笔发送幻灯片坐标,每个人的点按 meta.from 分开画;松开时再发一条关掉的快照。
| 做法 | 发出去的内容 | snap |
|---|---|---|
| 答题计票 | { t: "vote", q, choice }。收到后按 from 每人记一票 | 换题时把当前题号发出去 |
| 揭晓 | { t: "show", id, visible }。收到后 setVisible 或 triggerShape | 要。后进的人对上已经揭开的形状 |
| 提问 | 面板里的一行文本。主持人按 from 排成队列 | 队列还在时要 |
| 反应 | { t: "emoji", emoji }。在遮罩上短暂显示 | 不要。后进的人不必补看已经消失的表情 |
| 倒计时 | 结束时刻。各端用本地时钟相减 | 要 |
| 聚光 | 幻灯片坐标,画一个圈 | 松开时发一条关掉的快照 |
主持页和观众页跑的是这场快照里的放映插件。改了勾选或源码之后,重新推送这场快照,房间才换成新的插件。套餐允许把插件写入导出时,勾选的放映插件会打进这场 HTML。见 导出。
本地放映和导出的 HTML 里这条通道是空的,插件仍会运行。
definePlaybackPlugin({
id: "acme.vote",
name: "计票",
activate(ctx) {
const tally = new Map();
const off = ctx.live.on((data, meta) => {
if (!data || data.t !== "vote") return;
tally.set(meta.from, data.choice);
ctx.log(tally.size);
});
const offAttach = ctx.live.onAttach((on) => {
if (on) ctx.live.send({ t: "q", n: 1 }, { snap: true });
});
const button = ctx.ui.addToolbarButton({
label: "A",
onClick() {
ctx.live.send({ t: "vote", q: 1, choice: "A" });
},
});
return () => {
off();
offAttach();
button.remove();
};
},
});ctx.shared、ctx.locale、ctx.log
ctx.shared 是这一场里所有插件共用的一个对象。编辑器开的这场放映里,ctx.shared["ppt:fileName"] 是文件名。ctx.locale 是 "en" 或 "zh-CN"。ctx.log(...) 打印时带 [plugin:<id>] 前缀。