Skip to content

放映插件 ​

放映插件在放映时运行。它可以跟着当前页、控制翻页、读取这一页上的形状,并加上面板、工具栏按钮或一层遮罩。

官方放映插件随编辑器提供,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 个字符。

js
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。编辑插件不会。见 导出。

插件对象 ​

字段类型必填含义
idstring是稳定 id。字母开头,随后是字母、数字、.、_、-。不含 :。最长 64 个字符
namestring否插件列表里的名字。缺省用 id。截到 80 个字符
descriptionstring否名字下面的一行说明。截到 240 个字符
versionstring否你自己的版本号
activate(ctx) => cleanup | void | Promise<cleanup | void>是放映开始且这个插件被勾选时调用
deactivate() => void否清理函数之后调用,放映结束时会走到

activate(ctx) ​

字段用途
ctx.presentation页列表、尺寸、标题、备注。放映开始时的一份快照
ctx.player翻页,以及推进这一页的动画
ctx.eventsslidechange、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 ​

字段类型含义
idstring文稿 id
size{ width: number, height: number }幻灯片尺寸,像素
titlestring,可缺文档标题
slidesSlideInfo[]全部页,按文件顺序
visibleOrdernumber[]放映顺序下的绝对下标
metaobject,可缺作者、关键字等文档属性

SlideInfo:

字段类型含义
idstring页 id
namestring页名
indexnumber绝对下标
visibleIndexnumber在放映序列中的位置。隐藏页为 -1
showboolean隐藏页为 false
hasAnimationsboolean这一页有可以往下推进的动画步
totalAnimationStepsnumber步数。没有计时时为 0
notesstring,可缺演讲者备注
transition{ kind: string, duration: number },可缺切换名称,以及时长(秒)

ctx.player ​

函数参数返回作用
next()—void推进一个动画步。这一页没有剩余步时翻到下一页。已经是最后一页时触发 complete
prev()—void回到上一张可见页。不会把动画步倒退
goto(visibleIndex)visibleIndex: numbervoid跳到这张可见页,从 0 起
gotoById(slideId)slideId: stringboolean按页 id 跳。找不到返回 false
current()—number当前可见下标
total()—number可见页数
nextStep()—boolean只推进一个单击步,不翻页。这一页没有剩余步时返回 false
triggerShape(shapeId)shapeId: stringboolean播放这个形状的单击动画。shapeId 是 ShapeInfo.id。没有可播放的动画时返回 false
step()—{ totalSteps: number, currentStep: number }当前页的动画进度
freezeActive()—void把正在播的步全部停在末态
setNavigationLocked(locked)locked: booleanvoidtrue 时,单击翻页、键盘翻页、点形状触发动画和超链接都停用。next() 和 goto() 仍然可用
isNavigationLocked()—boolean导航是否锁着

如果锁过导航,在清理函数里解开。

ctx.events ​

函数参数返回
on(event, cb)event 是下表里的名字。cb 收到该事件的数据() => void,调用即取消订阅
off(event, cb)和 on 时相同的 event 与 cbvoid
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: stringShapeInfo | null名字完全相同的第一个形状
allByName(name)name: stringShapeInfo[]名字完全相同的全部形状
byId(id)id: stringShapeInfo | null这个渲染 id 对应的形状
hitTest(x, y)x: number, y: numberShapeInfo | null这个幻灯片坐标上最顶层的可见形状
toSlideCoords(clientX, clientY)clientX: number, clientY: number{ x, y } | null屏幕坐标换成幻灯片坐标。舞台还没有尺寸时返回 null
setVisible(id, visible)id: string, visible: booleanboolean显示或隐藏这个形状。找不到 id 返回 false
setOpacity(id, opacity)id: string, opacity: numberboolean透明度,0 到 1。找不到 id 返回 false
triggerShape(id)id: stringboolean与 ctx.player.triggerShape 相同

ShapeInfo:

字段类型含义
idstring渲染 id,例如 sp_12。传给 triggerShape
namestring选择窗格里的名字
kindstringtext、image、rect、ellipse、line、group、chart,以及其他形状种类
bbox{ x, y, w, h }幻灯片像素下的包围盒
visibleboolean当前是否显示
textstring,可缺纯文本。文字形状,以及里面有字的组合才有
hasClickAnimationbooleantriggerShape 能不能播放它的单击动画

先在选择窗格里给要用的形状起名,再用 byName 找。

ctx.ui ​

凡是往界面上加东西的调用,都返回 { remove(): void }。

函数参数返回作用
addPanel(opts)见下表{ remove() }停靠栏里一块可折叠面板
addToolbarButton(opts)见下表{ remove() }放映工具栏上的按钮
addOverlay(opts)见下表{ remove() }叠在幻灯片上的 HTML,用幻灯片坐标
mount(node)node: HTMLElement{ remove() }把 node 加到播放器根节点上
setDockOpen(open)open: booleanvoid打开或关掉停靠栏
toggleDock()—void切换停靠栏
isDockOpen()—boolean停靠栏是否开着
rootElement()—HTMLElement播放器根节点
stageElement()—HTMLElement幻灯片舞台

addPanel:

字段类型必填含义
titlestring是面板标题
render(body: HTMLElement) => void是创建时调用一次,用来填充内容
idstring否面板 id
initialOpenboolean否默认展开。设成 false 则一开始收起
ordernumber否数字越小越靠上。默认 100

addToolbarButton:

字段类型必填含义
labelstring是按钮文字
onClick(e: MouseEvent) => void是点击回调
tooltipstring否悬停文字
position"start" | "end"否工具栏左侧或右侧

addOverlay:

字段类型必填含义
nodeHTMLElement是要显示的元素
bbox{ x, y, w, h }否幻灯片像素。不写则铺满这一页
zIndexnumber否叠放顺序

ctx.contextMenu ​

函数参数返回作用
addItem(item)字段见下表{ remove() }加一行菜单
open(x, y)x: number, y: number,屏幕像素void在这个位置打开菜单
close()—void关掉菜单

addItem 的字段:

字段类型含义
idstring,可缺之后移除这一行时用
kind"item" | "checkbox" | "separator"默认 "item"
labelstring,可缺这一行的文字。分隔线可以不写
shortcutstring,可缺显示在右侧,放映时也会响应这个键。不带修饰键。一个字符,或者 ← → ↑ ↓ Esc Space Home End Enter F1 PageUp PageDown
ordernumber,可缺数字越小越靠上。默认 100
groupstring,可缺不同分组之间自动加一条分隔线
checkedboolean 或 () => boolean勾选状态。函数会在每次打开菜单时再算一次
disabledboolean 或 () => boolean函数会在每次打开菜单时再算一次
onClick(e?: MouseEvent) => voide 是这次右键,带 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?)同样的 optsstringSVG 文本
toSvgBlob(target, opts?)同样的 optsBlobimage/svg+xml
toDataUrl(target, opts?)同样的 optsstringSVG 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?: booleanvoid把 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 里这条通道是空的,插件仍会运行。

js
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>] 前缀。