@web-ppt/viewer-core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,45 @@
1
+ # 更新日志
2
+
3
+ 本文件记录对使用者可见的变化。版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
+
5
+ ## 0.3.0
6
+
7
+ 本版把 ECMA-376 的几处主要缺口补齐,并首次拿 **Apache POI 的 242 个公开测试文件**跑批验证:
8
+ 排除 22 个故意损坏的模糊测试样本后,真实文件成功率 **210/220 = 95.5%**。
9
+
10
+ ### 新增
11
+
12
+ - **预设几何补齐到规范全集**:163 → **187 个**。此前缺的 24 个走的是
13
+ `PRESETS[name] ?? PRESETS.rect`,会**静默画成矩形**,不报错也不进 `unsupported`——
14
+ `accentCallout*` 这类常见形状出现即无声画错
15
+ - **运动路径动画**:`p:animMotion@path` 此前从未被读取,`presetClass="path"` 一律退化成淡入。
16
+ 现按弧长等距重采样成位移点,播放层走 linear 缓动(PowerPoint 的路径动画是匀速)
17
+ - **切换效果 21 → 41 种**:补上 PowerPoint 2010+ 的全部 19 种 p14 扩展,以及 p159 的
18
+ **morph**——按形状 id 在前后两页之间配对做几何补间,不是淡入淡出
19
+ - **数学公式 OMML 真排版**:此前只把 `m:t` 拼成一行文字(分式变成 `a+b/2c`)。
20
+ 现做完整箱模型排版,覆盖分式 / 根式 / 上下标 / 大算符 / 定界符 / 矩阵 / 重音 / 极限等 12 类结构
21
+ - **加密文档**:
22
+ - `.pptx` 标准加密(AES-ECB)与敏捷加密(AES-CBC 分段)
23
+ - `.ppt` 老式 RC4 CryptoAPI(40 / 56 / 128 位)
24
+ - 用法 `parse(input, { password })`;密码错误抛 `WrongPasswordError`,与「文件损坏」区分开
25
+ - **PICT(Apple QuickDraw)解码器**:Mac 版 PowerPoint 存的 OLE 预览与图片此前只能是裂图
26
+ - **SmartArt 无缓存 drawing part 时自行排布**:python-pptx / Google Slides 导出的文件
27
+ 常常只写数据与版式定义,此前只能得到灰色占位框。现支持线性 / 循环 / 金字塔 / 层级 /
28
+ 蛇形 / 辐射六种布局族
29
+
30
+ ### 修复
31
+
32
+ - `borderCallout2` 的引线是三点**开放**子路径,会被 `fill` 补成实心楔形——引线画成了黑三角
33
+ - 后台标签页的 `document.timeline` 是暂停的,`Animation.finished` 永不 resolve,
34
+ 切换的收尾逻辑(移除旧图层)就永远不执行,配合自动换片会让图层持续堆积
35
+ - OLE 预览优先读 `p:oleObj` 内嵌的 `p:pic`(Office 2010+ 的主要写法),旧式 VML 快照作为后备
36
+
37
+ ### 其它
38
+
39
+ - `@web-ppt/core` gzip 68KB → 84KB(新增的解码器与公式排版;加密与 PICT 走 hook 注入,可 tree-shake)
40
+ - 测试:1298 → **1689 项断言**,快照 142 → 158 个
41
+ - `@web-ppt/viewer-core` 的 peer 依赖同步升到 `^0.3.0`
42
+
43
+ ## 0.2.0 及更早
44
+
45
+ 见 [提交历史](https://github.com/unStone/web-ppt/commits/master)。
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 探测当前引擎是否会给 <foreignObject> 里的 HTML 应用外层 SVG 的缩放。
3
+ *
4
+ * WebKit 从 2008 年起就不应用(bug 23113),直到新的 LBSE 引擎才修好。
5
+ * 本引擎的幻灯片是 `viewBox` + `width:100%`,也就是永远处在被缩放的状态,
6
+ * 所以在受影响的 Safari / iOS 上,foreignObject 里的文本会按 1× 排版并错位。
7
+ *
8
+ * Marp 的 marpit-svg-polyfill 用 getScreenCTM() 反算缩放再补回去,但那套做法
9
+ * 要求 foreignObject 就位于幻灯片原点;本引擎的 foreignObject 嵌在每个形状各自的
10
+ * translate/rotate 里,补偿量没法用一个标量表达。既然渲染层本来就有一条原生
11
+ * <text> 路径,受影响时直接切过去更稳——代价只是这些浏览器上文本不可选中。
12
+ */
13
+ /** 仅供测试:清掉探测缓存 */
14
+ export declare function resetForeignObjectProbe(): void;
15
+ /**
16
+ * `doc` 只在首次探测时用到——结论按引擎缓存,同一个页面里不会出现两种 SVG 实现,
17
+ * 没必要为每个 iframe / 游离文档各测一遍。
18
+ */
19
+ export declare function foreignObjectScalesCorrectly(doc: Document): boolean;
package/dist/index.d.ts CHANGED
@@ -8,6 +8,8 @@
8
8
  export { PresentationState } from './state';
9
9
  export type { PresentationStateOptions, StateChange } from './state';
10
10
  export { Viewer } from './viewer';
11
+ export { foreignObjectScalesCorrectly, resetForeignObjectProbe } from './foreign-object';
11
12
  export type { ViewerOptions } from './viewer';
12
- export { autoAdvanceMs, framesFor, groupSteps, hiddenBefore, playGroup, playTransition, } from './playback';
13
+ export { autoAdvanceMs, framesFor, morphPairs, playGroup, playTransition, transitionFrames } from './playback';
14
+ export { groupSteps, hiddenBefore } from '@web-ppt/core';
13
15
  export type { PlayHandle } from './playback';
@@ -4,16 +4,26 @@ interface Keyframes {
4
4
  to: Keyframe;
5
5
  }
6
6
  export declare function framesFor(step: AnimStep): Keyframes;
7
- /** 把动画按点击批次分组 */
8
- export declare function groupSteps(steps: AnimStep[] | undefined): AnimStep[][];
9
- /** 一批动画开始前,哪些元素应处于隐藏状态 */
10
- export declare function hiddenBefore(groups: AnimStep[][], upTo: number): Set<number>;
11
7
  export interface PlayHandle {
12
8
  cancel(): void;
13
9
  finished: Promise<void>;
14
10
  }
15
11
  /** 播放一批动画;返回可取消的句柄 */
16
12
  export declare function playGroup(container: Element, group: AnimStep[]): PlayHandle;
13
+ export declare function transitionFrames(t: Transition, incoming: boolean): Keyframe[];
14
+ /**
15
+ * morph:按 data-el(形状 id)在前后两页之间配对,配上的元素做几何补间,
16
+ * 其余淡入淡出。PowerPoint 的 morph 也是按形状 id 匹配的——morph 版面
17
+ * 基本都由「复制上一页再改」得来,id 天然一致。
18
+ *
19
+ * 配对成功的旧元素立刻置 0,新元素全程不透明,看起来才是"同一个东西在动",
20
+ * 而不是两层叠化。option="byWord"/"byChar" 的细粒度拆分不做,一律按对象处理。
21
+ */
22
+ export declare function morphPairs(outgoing: Element, incoming: Element): {
23
+ node: HTMLElement;
24
+ from: DOMRect;
25
+ to: DOMRect;
26
+ }[];
17
27
  /**
18
28
  * 播放切换:把旧内容截图式地留在下层,新内容在上层按效果进场。
19
29
  * outgoing 为旧内容节点(可为 null),incoming 为新内容节点。
@@ -1,28 +1,29 @@
1
- import { slideText as m, renderSlideToSvg as d, slideToPng as y } from "@web-ppt/core";
2
- const l = 1.15;
3
- function v(s) {
1
+ import { hiddenBefore as P, groupSteps as S, slideText as M, renderSlideToSvg as v, slideToPng as C } from "@web-ppt/core";
2
+ import { groupSteps as Z, hiddenBefore as R } from "@web-ppt/core";
3
+ const h = 1.15;
4
+ function T(s) {
4
5
  switch (s) {
5
6
  case "l":
6
- return [-100 * l, 0];
7
+ return [-100 * h, 0];
7
8
  case "r":
8
- return [100 * l, 0];
9
+ return [100 * h, 0];
9
10
  case "u":
10
- return [0, -100 * l];
11
+ return [0, -100 * h];
11
12
  case "d":
12
- return [0, 100 * l];
13
+ return [0, 100 * h];
13
14
  case "lu":
14
- return [-100 * l, -100 * l];
15
+ return [-100 * h, -100 * h];
15
16
  case "ru":
16
- return [100 * l, -100 * l];
17
+ return [100 * h, -100 * h];
17
18
  case "ld":
18
- return [-100 * l, 100 * l];
19
+ return [-100 * h, 100 * h];
19
20
  case "rd":
20
- return [100 * l, 100 * l];
21
+ return [100 * h, 100 * h];
21
22
  default:
22
- return [0, 100 * l];
23
+ return [0, 100 * h];
23
24
  }
24
25
  }
25
- function u(s, t) {
26
+ function d(s, t) {
26
27
  const e = t ? 100 : 0;
27
28
  switch (s) {
28
29
  case "l":
@@ -39,12 +40,12 @@ function u(s, t) {
39
40
  return `inset(${e}% 0 0 0)`;
40
41
  }
41
42
  }
42
- function f(s) {
43
+ function w(s) {
43
44
  switch (s.effect) {
44
45
  case "appear":
45
46
  return { from: { opacity: 0 }, to: { opacity: 1 } };
46
47
  case "fly": {
47
- const [t, e] = v(s.dir);
48
+ const [t, e] = T(s.dir);
48
49
  return {
49
50
  from: { opacity: 0, transform: `translate(${t}%, ${e}%)` },
50
51
  to: { opacity: 1, transform: "translate(0, 0)" }
@@ -72,8 +73,8 @@ function f(s) {
72
73
  case "split":
73
74
  case "wheel":
74
75
  return {
75
- from: { opacity: 1, clipPath: u(s.dir, !0) },
76
- to: { opacity: 1, clipPath: u(s.dir, !1) }
76
+ from: { opacity: 1, clipPath: d(s.dir, !0) },
77
+ to: { opacity: 1, clipPath: d(s.dir, !1) }
77
78
  };
78
79
  case "dissolve":
79
80
  case "fade":
@@ -82,11 +83,11 @@ function f(s) {
82
83
  return { from: { opacity: 0 }, to: { opacity: 1 } };
83
84
  }
84
85
  }
85
- function g(s) {
86
- const t = f({ ...s });
86
+ function $(s) {
87
+ const t = w({ ...s });
87
88
  return { from: t.to, to: t.from };
88
89
  }
89
- function x(s) {
90
+ function k(s) {
90
91
  switch (s.effect) {
91
92
  case "spin":
92
93
  return { from: { transform: "rotate(0)" }, to: { transform: "rotate(360deg)" } };
@@ -96,51 +97,35 @@ function x(s) {
96
97
  return { from: { opacity: 1 }, to: { opacity: 0.35 } };
97
98
  }
98
99
  }
99
- function A(s) {
100
- return s.kind === "exit" ? g(s) : s.kind === "emphasis" ? x(s) : f(s);
101
- }
102
- function w(s) {
103
- if (!(s != null && s.length)) return [];
104
- const t = [];
105
- for (const e of s) {
106
- const n = e.clickGroup ?? 0;
107
- (t[n] ?? (t[n] = [])).push(e);
108
- }
109
- return t.filter(Boolean);
110
- }
111
- function b(s, t) {
112
- const e = /* @__PURE__ */ new Set();
113
- for (let n = t; n < s.length; n++)
114
- for (const i of s[n]) i.kind === "entrance" && e.add(i.target);
115
- for (let n = 0; n < t; n++)
116
- for (const i of s[n]) i.kind === "exit" && e.add(i.target);
117
- for (let n = t; n < s.length; n++)
118
- for (const i of s[n]) i.kind === "entrance" && e.add(i.target);
119
- return e;
100
+ function E(s) {
101
+ return s.kind === "exit" ? $(s) : s.kind === "emphasis" ? k(s) : s.kind === "motion" ? { from: { opacity: 1 }, to: { opacity: 1 } } : w(s);
120
102
  }
121
- function T(s, t) {
103
+ function z(s, t) {
104
+ var n, r;
122
105
  const e = [];
123
- let n = 0;
106
+ let a = 0;
124
107
  for (const i of t) {
125
- const r = s.querySelector(`[data-el="${i.target}"]`);
126
- if (!r) continue;
127
- const a = i.trigger === "afterPrev" ? n + i.delayMs : i.delayMs;
128
- i.trigger === "afterPrev" ? n = a + i.durationMs : n = Math.max(n, a + i.durationMs);
129
- const { from: c, to: o } = A(i);
130
- r.style.visibility = "visible", i.kind === "entrance" && (r.style.opacity = "");
108
+ const c = s.querySelector(`[data-el="${i.target}"]`);
109
+ if (!c) continue;
110
+ const o = i.trigger === "afterPrev" ? a + i.delayMs : i.delayMs;
111
+ i.trigger === "afterPrev" ? a = o + i.durationMs : a = Math.max(a, o + i.durationMs);
112
+ const { from: l, to: p } = E(i);
113
+ c.style.visibility = "visible", i.kind === "entrance" && (c.style.opacity = "");
114
+ const f = (n = i.motionPath) != null && n.length ? i.motionPath.map(([u, y]) => ({ transform: `translate(${u}px, ${y}px)` })) : [l, p];
131
115
  try {
132
- const h = r.animate([c, o], {
116
+ const u = c.animate(f, {
133
117
  duration: i.durationMs,
134
- delay: a,
135
- easing: "cubic-bezier(0.25, 0.46, 0.45, 0.94)",
118
+ delay: o,
119
+ // 路径动画在 PowerPoint 里是匀速,不能套入场用的缓动
120
+ easing: (r = i.motionPath) != null && r.length ? "linear" : "cubic-bezier(0.25, 0.46, 0.45, 0.94)",
136
121
  fill: "both"
137
122
  });
138
- i.kind === "exit" && h.finished.then(() => {
139
- r.style.visibility = "hidden";
123
+ i.kind === "exit" && u.finished.then(() => {
124
+ c.style.visibility = "hidden";
140
125
  }).catch(() => {
141
- }), e.push(h);
126
+ }), e.push(u);
142
127
  } catch {
143
- Object.assign(r.style, o);
128
+ Object.assign(c.style, f[f.length - 1] ?? p);
144
129
  }
145
130
  }
146
131
  return {
@@ -155,27 +140,27 @@ function T(s, t) {
155
140
  })
156
141
  };
157
142
  }
158
- function p(s, t) {
159
- const e = s.dir ?? "", n = (r, a) => `translate(${r}, ${a})`, i = () => e.includes("l") ? ["-100%", "0"] : e.includes("r") ? ["100%", "0"] : e.includes("u") ? ["0", "-100%"] : ["0", "100%"];
143
+ function x(s, t) {
144
+ const e = s.dir ?? "", a = (r, i) => `translate(${r}, ${i})`, n = () => e.includes("l") ? ["-100%", "0"] : e.includes("r") ? ["100%", "0"] : e.includes("u") ? ["0", "-100%"] : ["0", "100%"];
160
145
  switch (s.type) {
161
146
  case "cut":
162
147
  return t ? [{ opacity: 1 }, { opacity: 1 }] : [{ opacity: 0 }, { opacity: 0 }];
163
148
  case "push":
164
149
  case "pull": {
165
- const [r, a] = i(), c = s.type === "pull" ? -1 : 1, o = (h) => h === "0" ? "0" : `${-parseFloat(h) * c}%`;
166
- return t ? [{ transform: n(c > 0 ? r : o(r), c > 0 ? a : o(a)) }, { transform: "translate(0,0)" }] : [{ transform: "translate(0,0)" }, { transform: n(o(r), o(a)) }];
150
+ const [r, i] = n(), c = s.type === "pull" ? -1 : 1, o = (l) => l === "0" ? "0" : `${-parseFloat(l) * c}%`;
151
+ return t ? [{ transform: a(c > 0 ? r : o(r), c > 0 ? i : o(i)) }, { transform: "translate(0,0)" }] : [{ transform: "translate(0,0)" }, { transform: a(o(r), o(i)) }];
167
152
  }
168
153
  case "cover": {
169
- const [r, a] = i();
170
- return t ? [{ transform: n(r, a) }, { transform: "translate(0,0)" }] : [{ opacity: 1 }, { opacity: 1 }];
154
+ const [r, i] = n();
155
+ return t ? [{ transform: a(r, i) }, { transform: "translate(0,0)" }] : [{ opacity: 1 }, { opacity: 1 }];
171
156
  }
172
157
  case "wipe": {
173
- const r = u(e || "d", !0), a = u(e || "d", !1);
174
- return t ? [{ clipPath: r }, { clipPath: a }] : [{ opacity: 1 }, { opacity: 1 }];
158
+ const r = d(e || "d", !0), i = d(e || "d", !1);
159
+ return t ? [{ clipPath: r }, { clipPath: i }] : [{ opacity: 1 }, { opacity: 1 }];
175
160
  }
176
161
  case "split": {
177
- const r = e.startsWith("vert"), a = e.endsWith("out"), c = r ? "inset(0 50% 0 50%)" : "inset(50% 0 50% 0)", o = "inset(0 0 0 0)";
178
- return t ? a ? [{ clipPath: c }, { clipPath: o }] : [{ clipPath: o }, { clipPath: o }] : [{ clipPath: o }, { clipPath: c }];
162
+ const r = e.startsWith("vert"), i = e.endsWith("out"), c = r ? "inset(0 50% 0 50%)" : "inset(50% 0 50% 0)", o = "inset(0 0 0 0)";
163
+ return t ? i ? [{ clipPath: c }, { clipPath: o }] : [{ clipPath: o }, { clipPath: o }] : [{ clipPath: o }, { clipPath: c }];
179
164
  }
180
165
  case "zoom":
181
166
  return t ? [{ opacity: 0, transform: e === "out" ? "scale(1.4)" : "scale(0.6)" }, { opacity: 1, transform: "scale(1)" }] : [{ opacity: 1 }, { opacity: 0 }];
@@ -185,6 +170,67 @@ function p(s, t) {
185
170
  case "plus":
186
171
  case "wedge":
187
172
  return t ? [{ opacity: 0, transform: "scale(0.7)" }, { opacity: 1, transform: "scale(1)" }] : [{ opacity: 1 }, { opacity: 0 }];
173
+ case "ripple":
174
+ return t ? [{ clipPath: "circle(0% at 50% 50%)" }, { clipPath: "circle(75% at 50% 50%)" }] : [{ opacity: 1 }, { opacity: 1 }];
175
+ case "vortex":
176
+ return t ? [{ opacity: 0, transform: "scale(0.2) rotate(-180deg)" }, { opacity: 1, transform: "none" }] : [{ opacity: 1 }, { opacity: 0 }];
177
+ case "honeycomb":
178
+ return t ? [{ opacity: 0, transform: "scale(0.85) rotate(-6deg)" }, { opacity: 1, transform: "none" }] : [{ opacity: 1, transform: "none" }, { opacity: 0, transform: "scale(1.1)" }];
179
+ case "glitter":
180
+ return t ? [{ opacity: 0, clipPath: d(e || "l", !0) }, { opacity: 1, clipPath: d(e || "l", !1) }] : [{ opacity: 1 }, { opacity: 0 }];
181
+ case "shred":
182
+ return t ? [{ opacity: 0, transform: "scale(1.06)" }, { opacity: 1, transform: "none" }] : [{ opacity: 1, transform: "none" }, { opacity: 0, transform: "scaleY(0.9)" }];
183
+ case "flash":
184
+ return t ? [{ opacity: 0, filter: "brightness(3)" }, { opacity: 1, filter: "brightness(1)" }] : [{ opacity: 1, filter: "brightness(1)" }, { opacity: 0, filter: "brightness(3)" }];
185
+ case "reveal": {
186
+ const [r, i] = n();
187
+ return t ? [{ opacity: 0, transform: a(r, i) }, { opacity: 1, transform: "translate(0,0)" }] : [{ opacity: 1 }, { opacity: 0 }];
188
+ }
189
+ case "wheelReverse":
190
+ return t ? [{ opacity: 0, transform: "scale(1.3) rotate(20deg)" }, { opacity: 1, transform: "none" }] : [{ opacity: 1 }, { opacity: 0 }];
191
+ case "doors":
192
+ return t ? [{ clipPath: "inset(0 50% 0 50%)" }, { clipPath: "inset(0 0 0 0)" }] : [{ opacity: 1 }, { opacity: 1 }];
193
+ case "window":
194
+ return t ? [{ clipPath: "inset(50% 0 50% 0)" }, { clipPath: "inset(0 0 0 0)" }] : [{ opacity: 1 }, { opacity: 1 }];
195
+ case "switch":
196
+ case "flip":
197
+ case "prism": {
198
+ const r = s.type === "flip" ? -1 : 1;
199
+ return t ? [
200
+ { opacity: 0, transform: `perspective(1400px) rotateY(${r * -80}deg)` },
201
+ { opacity: 1, transform: "perspective(1400px) rotateY(0)" }
202
+ ] : [
203
+ { opacity: 1, transform: "perspective(1400px) rotateY(0)" },
204
+ { opacity: 0, transform: `perspective(1400px) rotateY(${r * 80}deg)` }
205
+ ];
206
+ }
207
+ case "ferris":
208
+ return t ? [
209
+ { opacity: 0, transform: "perspective(1400px) rotateX(70deg)" },
210
+ { opacity: 1, transform: "perspective(1400px) rotateX(0)" }
211
+ ] : [
212
+ { opacity: 1, transform: "perspective(1400px) rotateX(0)" },
213
+ { opacity: 0, transform: "perspective(1400px) rotateX(-70deg)" }
214
+ ];
215
+ case "gallery":
216
+ case "conveyor": {
217
+ const r = s.type === "conveyor" ? 25 : 12, i = e.includes("l") ? "-100%" : "100%", c = e.includes("l") ? "100%" : "-100%";
218
+ return t ? [
219
+ { transform: `perspective(1600px) translateX(${i}) rotateY(${r}deg)` },
220
+ { transform: "perspective(1600px) translateX(0) rotateY(0)" }
221
+ ] : [
222
+ { transform: "perspective(1600px) translateX(0) rotateY(0)" },
223
+ { transform: `perspective(1600px) translateX(${c}) rotateY(${-r}deg)` }
224
+ ];
225
+ }
226
+ case "pan": {
227
+ const [r, i] = n(), c = (o) => o === "0" ? "0" : `${-parseFloat(o)}%`;
228
+ return t ? [{ transform: a(r, i) }, { transform: "translate(0,0)" }] : [{ transform: "translate(0,0)" }, { transform: a(c(r), c(i)) }];
229
+ }
230
+ case "flythrough":
231
+ return t ? [{ opacity: 0, transform: "perspective(1000px) scale(0.15)" }, { opacity: 1, transform: "none" }] : [{ opacity: 1, transform: "none" }, { opacity: 0, transform: "perspective(1000px) scale(2.2)" }];
232
+ case "warp":
233
+ return t ? [{ opacity: 0, transform: "skewX(-18deg) scale(1.15)" }, { opacity: 1, transform: "none" }] : [{ opacity: 1, transform: "none" }, { opacity: 0, transform: "skewX(18deg) scale(0.9)" }];
188
234
  case "blinds":
189
235
  case "checker":
190
236
  case "comb":
@@ -193,30 +239,75 @@ function p(s, t) {
193
239
  case "newsflash":
194
240
  case "dissolve":
195
241
  case "fade":
242
+ case "morph":
196
243
  default:
197
244
  return t ? [{ opacity: 0 }, { opacity: 1 }] : [{ opacity: 1 }, { opacity: 0 }];
198
245
  }
199
246
  }
200
- function k(s, t, e) {
247
+ function A(s, t) {
248
+ const e = Promise.all(s.map((a) => a.catch(() => {
249
+ }))).then(() => {
250
+ });
251
+ return typeof setTimeout != "function" ? e : Promise.race([e, new Promise((a) => setTimeout(a, t + 250))]);
252
+ }
253
+ function Y(s, t) {
254
+ const e = /* @__PURE__ */ new Map();
255
+ s.querySelectorAll("[data-el]").forEach((n) => {
256
+ const r = n.getBoundingClientRect();
257
+ (r.width > 0 || r.height > 0) && e.set(n.getAttribute("data-el") ?? "", r);
258
+ });
259
+ const a = [];
260
+ return t.querySelectorAll("[data-el]").forEach((n) => {
261
+ const r = e.get(n.getAttribute("data-el") ?? "");
262
+ if (!r) return;
263
+ const i = n.getBoundingClientRect();
264
+ i.width <= 0 && i.height <= 0 || a.push({ node: n, from: r, to: i });
265
+ }), a;
266
+ }
267
+ function L(s, t, e) {
268
+ if (!s) return Promise.resolve();
269
+ const a = { duration: e.durationMs, easing: "ease-in-out", fill: "both" }, n = Y(s, t), r = new Set(n.map((o) => o.node.getAttribute("data-el"))), i = [], c = (o, l) => {
270
+ try {
271
+ i.push(o.animate(l, a).finished);
272
+ } catch {
273
+ }
274
+ };
275
+ for (const { node: o, from: l, to: p } of n) {
276
+ const f = l.left - p.left, u = l.top - p.top, y = p.width > 0 ? l.width / p.width : 1, b = p.height > 0 ? l.height / p.height : 1;
277
+ c(o, [
278
+ { transformOrigin: "left top", transform: `translate(${f}px, ${u}px) scale(${y}, ${b})` },
279
+ { transformOrigin: "left top", transform: "none" }
280
+ ]);
281
+ }
282
+ return s.querySelectorAll("[data-el]").forEach((o) => {
283
+ const l = r.has(o.getAttribute("data-el"));
284
+ c(o, l ? [{ opacity: 0 }, { opacity: 0 }] : [{ opacity: 1 }, { opacity: 0 }]);
285
+ }), t.querySelectorAll("[data-el]").forEach((o) => {
286
+ r.has(o.getAttribute("data-el")) || c(o, [{ opacity: 0 }, { opacity: 1 }]);
287
+ }), A(i, e.durationMs).then(() => {
288
+ s.remove();
289
+ });
290
+ }
291
+ function X(s, t, e) {
201
292
  if (!e || e.type === "none")
202
293
  return s == null || s.remove(), Promise.resolve();
203
- const n = { duration: e.durationMs, easing: "ease-in-out", fill: "both" }, i = [];
294
+ if (e.type === "morph" && s) return L(s, t, e);
295
+ const a = { duration: e.durationMs, easing: "ease-in-out", fill: "both" }, n = [];
204
296
  try {
205
- i.push(t.animate(p(e, !0), n).finished), s && i.push(s.animate(p(e, !1), n).finished);
297
+ n.push(t.animate(x(e, !0), a).finished), s && n.push(s.animate(x(e, !1), a).finished);
206
298
  } catch {
207
299
  return s == null || s.remove(), Promise.resolve();
208
300
  }
209
- return Promise.all(i.map((r) => r.catch(() => {
210
- }))).then(() => {
301
+ return A(n, e.durationMs).then(() => {
211
302
  s == null || s.remove();
212
303
  });
213
304
  }
214
- function C(s) {
305
+ function N(s) {
215
306
  var e;
216
307
  const t = (e = s.transition) == null ? void 0 : e.advanceAfterMs;
217
308
  return t !== void 0 && t > 0 ? t : null;
218
309
  }
219
- class P {
310
+ class V {
220
311
  constructor(t, e = {}) {
221
312
  this.presentation = t, this.options = e, this.idx = 0, this.zoomValue = 1, this.groups = [], this.cursor = 0, this.listeners = /* @__PURE__ */ new Set(), this.autoTimer = null, this.textCache = /* @__PURE__ */ new Map(), this.idx = this.clamp(e.index ?? 0), this.loadAnimations();
222
313
  }
@@ -247,11 +338,11 @@ class P {
247
338
  }
248
339
  /** 当前批次下应当隐藏的元素 id —— UI 据此设置可见性 */
249
340
  get hiddenElementIds() {
250
- return this.animate ? b(this.groups, this.cursor) : /* @__PURE__ */ new Set();
341
+ return this.animate ? P(this.groups, this.cursor) : /* @__PURE__ */ new Set();
251
342
  }
252
343
  /** 本页的自动换片延迟(毫秒),未配置则为 null */
253
344
  get autoAdvanceDelay() {
254
- return this.options.autoAdvance ? C(this.slide) : null;
345
+ return this.options.autoAdvance ? N(this.slide) : null;
255
346
  }
256
347
  // ---------------- 订阅 ----------------
257
348
  subscribe(t) {
@@ -273,13 +364,13 @@ class P {
273
364
  * 前进时带上目标页的切换效果;回退不播切换,避免观感错乱。
274
365
  */
275
366
  goTo(t, e = "forward") {
276
- const n = this.clamp(t);
277
- if (n === this.idx) return !1;
278
- const i = this.idx;
279
- return this.idx = n, this.loadAnimations(), this.emit({
367
+ const a = this.clamp(t);
368
+ if (a === this.idx) return !1;
369
+ const n = this.idx;
370
+ return this.idx = a, this.loadAnimations(), this.emit({
280
371
  type: "slide",
281
- index: n,
282
- previous: i,
372
+ index: a,
373
+ previous: n,
283
374
  transition: e === "forward" && this.animate ? this.slide.transition : void 0
284
375
  }), !0;
285
376
  }
@@ -304,7 +395,7 @@ class P {
304
395
  }
305
396
  // ---------------- 动画 ----------------
306
397
  loadAnimations() {
307
- this.groups = this.animate ? w(this.slide.animations) : [], this.cursor = 0;
398
+ this.groups = this.animate ? S(this.slide.animations) : [], this.cursor = 0;
308
399
  }
309
400
  /**
310
401
  * 推进一批动画。返回应播放的这一批;没有待播批次时返回 null。
@@ -335,42 +426,62 @@ class P {
335
426
  // ---------------- 文本 / 搜索 ----------------
336
427
  text(t) {
337
428
  let e = this.textCache.get(t);
338
- return e === void 0 && (e = m(this.presentation.slides[t]), this.textCache.set(t, e)), e;
429
+ return e === void 0 && (e = M(this.presentation.slides[t]), this.textCache.set(t, e)), e;
339
430
  }
340
431
  /** 全文搜索,返回命中的页索引 */
341
432
  search(t) {
342
433
  const e = t.trim().toLowerCase();
343
434
  if (!e) return [];
344
- const n = [];
345
- for (let i = 0; i < this.count; i++)
346
- this.text(i).toLowerCase().includes(e) && n.push(i);
347
- return n;
435
+ const a = [];
436
+ for (let n = 0; n < this.count; n++)
437
+ this.text(n).toLowerCase().includes(e) && a.push(n);
438
+ return a;
348
439
  }
349
440
  /** 解析内部跳转目标(`slide:3` / `slide:last`)为页索引;外部链接返回 null */
350
441
  resolveLink(t) {
351
442
  if (!t.startsWith("slide:")) return null;
352
- const e = t.slice(6), n = e === "last" ? this.count : Number(e);
353
- return Number.isFinite(n) ? this.clamp(n - 1) : null;
443
+ const e = t.slice(6), a = e === "last" ? this.count : Number(e);
444
+ return Number.isFinite(a) ? this.clamp(a - 1) : null;
354
445
  }
355
446
  destroy() {
356
447
  this.cancelAutoAdvance(), this.listeners.clear(), this.textCache.clear();
357
448
  }
358
449
  }
359
- class M {
360
- constructor(t, e, n = {}) {
361
- this.container = t, this.presentation = e, this.svgCache = /* @__PURE__ */ new Map(), this.playing = null, this.cancelAuto = null, this.onChange = null, this.onAnimStep = null, this.onLinkClick = null, this.handleClick = (i) => {
362
- const r = i.target, a = r == null ? void 0 : r.closest("[data-slide]");
363
- if (a) {
364
- i.preventDefault();
365
- const h = this.state.resolveLink(`slide:${a.getAttribute("data-slide") ?? ""}`);
366
- h !== null && this.goTo(h);
450
+ const g = "http://www.w3.org/2000/svg", F = "http://www.w3.org/1999/xhtml";
451
+ let m = null;
452
+ function j() {
453
+ m = null;
454
+ }
455
+ function D(s) {
456
+ if (m !== null) return m;
457
+ const t = s.body ?? s.documentElement;
458
+ if (!t) return !0;
459
+ const e = s.createElementNS(g, "svg");
460
+ e.setAttribute("viewBox", "0 0 100 100"), e.setAttribute("width", "200"), e.setAttribute("height", "200"), e.setAttribute("style", "position:absolute;left:-9999px;top:-9999px;pointer-events:none");
461
+ const a = s.createElementNS(g, "foreignObject");
462
+ a.setAttribute("width", "100"), a.setAttribute("height", "100");
463
+ const n = s.createElementNS(F, "div");
464
+ n.setAttribute("style", "width:100px;height:100px"), a.appendChild(n), e.appendChild(a), t.appendChild(e);
465
+ const r = n.getBoundingClientRect().width;
466
+ return e.remove(), m = r === 0 || r > 150, m;
467
+ }
468
+ class O {
469
+ constructor(t, e, a = {}) {
470
+ this.container = t, this.presentation = e, this.svgCache = /* @__PURE__ */ new Map(), this.playing = null, this.cancelAuto = null, this.onChange = null, this.onAnimStep = null, this.onLinkClick = null, this.handleClick = (r) => {
471
+ const i = r.target, c = i == null ? void 0 : i.closest("[data-slide]");
472
+ if (c) {
473
+ r.preventDefault();
474
+ const p = this.state.resolveLink(`slide:${c.getAttribute("data-slide") ?? ""}`);
475
+ p !== null && this.goTo(p);
367
476
  return;
368
477
  }
369
- const c = r == null ? void 0 : r.closest("a[href]"), o = c == null ? void 0 : c.getAttribute("href");
370
- o && this.onLinkClick && this.onLinkClick(o) === !0 && i.preventDefault();
371
- }, this.state = new P(e, n), this.unsubscribe = this.state.subscribe((i) => {
372
- var r, a;
373
- i.type === "slide" ? (this.paint(i.transition), (r = this.onChange) == null || r.call(this, i.index)) : i.type === "animation" ? (i.group ? this.play(i.group) : this.applyVisibility(), (a = this.onAnimStep) == null || a.call(this, i.done, i.total)) : i.type === "zoom" && this.applyZoom();
478
+ const o = i == null ? void 0 : i.closest("a[href]"), l = o == null ? void 0 : o.getAttribute("href");
479
+ l && this.onLinkClick && this.onLinkClick(l) === !0 && r.preventDefault();
480
+ }, this.media = a.media ?? "badge";
481
+ const n = a.textMode ?? "auto";
482
+ this.textMode = n !== "auto" ? n : D(t.ownerDocument) ? "html" : "svg", this.state = new V(e, a), this.unsubscribe = this.state.subscribe((r) => {
483
+ var i, c;
484
+ r.type === "slide" ? (this.paint(r.transition), (i = this.onChange) == null || i.call(this, r.index)) : r.type === "animation" ? (r.group ? this.play(r.group) : this.applyVisibility(), (c = this.onAnimStep) == null || c.call(this, r.done, r.total)) : r.type === "zoom" && this.applyZoom();
374
485
  }), this.container.addEventListener("click", this.handleClick), this.paint();
375
486
  }
376
487
  // ---------------- 转发给状态机 ----------------
@@ -426,36 +537,39 @@ class M {
426
537
  /** 带缓存的单页 SVG(主视图用) */
427
538
  slideSvg(t) {
428
539
  let e = this.svgCache.get(t);
429
- return e === void 0 && (e = d(this.presentation, this.presentation.slides[t]), this.svgCache.set(t, e)), e;
540
+ return e === void 0 && (e = v(this.presentation, this.presentation.slides[t], {
541
+ media: this.media,
542
+ textMode: this.textMode
543
+ }), this.svgCache.set(t, e)), e;
430
544
  }
431
545
  /** 不走缓存,用于缩略图等需要独立 defs id 的场景 */
432
546
  renderSlide(t) {
433
- return d(this.presentation, this.presentation.slides[t]);
547
+ return v(this.presentation, this.presentation.slides[t], { textMode: this.textMode });
434
548
  }
435
549
  exportPng(t = 2, e = this.index) {
436
- return y(this.presentation, this.presentation.slides[e], t);
550
+ return C(this.presentation, this.presentation.slides[e], t);
437
551
  }
438
552
  paint(t) {
439
- var a, c, o;
440
- (a = this.playing) == null || a.cancel(), this.playing = null, (c = this.cancelAuto) == null || c.call(this);
553
+ var i, c, o;
554
+ (i = this.playing) == null || i.cancel(), this.playing = null, (c = this.cancelAuto) == null || c.call(this);
441
555
  const e = document.createElement("div");
442
556
  e.style.cssText = "position:absolute;inset:0;display:flex;align-items:center;justify-content:center", e.innerHTML = this.slideSvg(this.index);
443
- const n = this.container.firstElementChild, i = this.container.ownerDocument.defaultView;
444
- (!i || i.getComputedStyle(this.container).position === "static") && (this.container.style.position = "relative"), t && n ? (this.container.appendChild(e), k(n, e, t).then(() => this.startAutoAdvance())) : (this.container.innerHTML = "", this.container.appendChild(e), this.startAutoAdvance());
557
+ const a = this.container.firstElementChild, n = this.container.ownerDocument.defaultView;
558
+ (!n || n.getComputedStyle(this.container).position === "static") && (this.container.style.position = "relative"), t && a ? (this.container.appendChild(e), X(a, e, t).then(() => this.startAutoAdvance())) : (this.container.innerHTML = "", this.container.appendChild(e), this.startAutoAdvance());
445
559
  const r = e.querySelector("svg");
446
560
  r && (r.style.display = "block", r.style.maxWidth = "none"), this.applyZoom(), this.applyVisibility(), (o = this.onAnimStep) == null || o.call(this, this.state.animationDone, this.state.animationTotal);
447
561
  }
448
562
  play(t) {
449
563
  var e;
450
- (e = this.playing) == null || e.cancel(), this.playing = T(this.container, t), this.applyVisibility();
564
+ (e = this.playing) == null || e.cancel(), this.playing = z(this.container, t), this.applyVisibility();
451
565
  }
452
566
  /** 按状态机给出的隐藏集合设置元素可见性 */
453
567
  applyVisibility() {
454
568
  if (!this.state.animate) return;
455
569
  const t = this.state.hiddenElementIds;
456
570
  this.container.querySelectorAll("[data-el]").forEach((e) => {
457
- const n = Number(e.getAttribute("data-el"));
458
- e.style.visibility = t.has(n) ? "hidden" : "visible";
571
+ const a = Number(e.getAttribute("data-el"));
572
+ e.style.visibility = t.has(a) ? "hidden" : "visible";
459
573
  });
460
574
  }
461
575
  applyZoom() {
@@ -473,12 +587,16 @@ class M {
473
587
  }
474
588
  }
475
589
  export {
476
- P as PresentationState,
477
- M as Viewer,
478
- C as autoAdvanceMs,
479
- A as framesFor,
480
- w as groupSteps,
481
- b as hiddenBefore,
482
- T as playGroup,
483
- k as playTransition
590
+ V as PresentationState,
591
+ O as Viewer,
592
+ N as autoAdvanceMs,
593
+ D as foreignObjectScalesCorrectly,
594
+ E as framesFor,
595
+ Z as groupSteps,
596
+ R as hiddenBefore,
597
+ Y as morphPairs,
598
+ z as playGroup,
599
+ X as playTransition,
600
+ j as resetForeignObjectProbe,
601
+ x as transitionFrames
484
602
  };
package/dist/viewer.d.ts CHANGED
@@ -8,12 +8,25 @@ import { PresentationState, type PresentationStateOptions } from './state';
8
8
  * 因此换成 React / Vue 只需重写这三件事。
9
9
  */
10
10
  export interface ViewerOptions extends PresentationStateOptions {
11
+ /**
12
+ * 媒体呈现:'badge' 只画封面帧与播放标识,'player' 嵌入可播放的
13
+ * <video>/<audio>。导出(exportPng)不受影响,始终走 badge。
14
+ */
15
+ media?: 'badge' | 'player';
16
+ /**
17
+ * 文本渲染方式。默认 'auto':探测到引擎不给 foreignObject 应用 SVG 缩放
18
+ * (WebKit bug 23113)时自动切到原生 <text>,否则用 foreignObject。
19
+ * 传死值可绕过探测。
20
+ */
21
+ textMode?: 'auto' | 'html' | 'svg';
11
22
  }
12
23
  export declare class Viewer {
13
24
  private container;
14
25
  readonly presentation: Presentation;
15
26
  readonly state: PresentationState;
16
27
  private svgCache;
28
+ private media;
29
+ private textMode;
17
30
  private playing;
18
31
  private cancelAuto;
19
32
  private unsubscribe;
package/package.json CHANGED
@@ -1,12 +1,22 @@
1
1
  {
2
2
  "name": "@web-ppt/viewer-core",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Web-PPT 查看器的 headless 状态机与播放层:导航 / 缩放 / 搜索 / 动画批次,可被任意 UI 框架驱动",
5
5
  "keywords": [
6
6
  "pptx",
7
+ "ppt",
8
+ "powerpoint",
7
9
  "viewer",
8
10
  "headless",
9
- "state-machine"
11
+ "state-machine",
12
+ "presentation",
13
+ "slideshow",
14
+ "svg",
15
+ "browser",
16
+ "react",
17
+ "vue",
18
+ "svelte",
19
+ "typescript"
10
20
  ],
11
21
  "license": "MIT",
12
22
  "author": "unStone",
@@ -31,14 +41,16 @@
31
41
  "files": [
32
42
  "dist",
33
43
  "README.md",
44
+ "CHANGELOG.md",
34
45
  "LICENSE"
35
46
  ],
36
47
  "scripts": {
37
48
  "build": "vite build && tsc -p tsconfig.build.json",
49
+ "prepublishOnly": "npm run build",
38
50
  "prepack": "node ../../tooling/sync-package-docs.mjs"
39
51
  },
40
52
  "peerDependencies": {
41
- "@web-ppt/core": "^0.1.0"
53
+ "@web-ppt/core": "^0.3.0"
42
54
  },
43
55
  "publishConfig": {
44
56
  "access": "public"