@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 +45 -0
- package/dist/foreign-object.d.ts +19 -0
- package/dist/index.d.ts +3 -1
- package/dist/playback.d.ts +14 -4
- package/dist/viewer-core.js +238 -120
- package/dist/viewer.d.ts +13 -0
- package/package.json +15 -3
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,
|
|
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';
|
package/dist/playback.d.ts
CHANGED
|
@@ -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 为新内容节点。
|
package/dist/viewer-core.js
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
import { slideText as
|
|
2
|
-
|
|
3
|
-
|
|
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 *
|
|
7
|
+
return [-100 * h, 0];
|
|
7
8
|
case "r":
|
|
8
|
-
return [100 *
|
|
9
|
+
return [100 * h, 0];
|
|
9
10
|
case "u":
|
|
10
|
-
return [0, -100 *
|
|
11
|
+
return [0, -100 * h];
|
|
11
12
|
case "d":
|
|
12
|
-
return [0, 100 *
|
|
13
|
+
return [0, 100 * h];
|
|
13
14
|
case "lu":
|
|
14
|
-
return [-100 *
|
|
15
|
+
return [-100 * h, -100 * h];
|
|
15
16
|
case "ru":
|
|
16
|
-
return [100 *
|
|
17
|
+
return [100 * h, -100 * h];
|
|
17
18
|
case "ld":
|
|
18
|
-
return [-100 *
|
|
19
|
+
return [-100 * h, 100 * h];
|
|
19
20
|
case "rd":
|
|
20
|
-
return [100 *
|
|
21
|
+
return [100 * h, 100 * h];
|
|
21
22
|
default:
|
|
22
|
-
return [0, 100 *
|
|
23
|
+
return [0, 100 * h];
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
|
-
function
|
|
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
|
|
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] =
|
|
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:
|
|
76
|
-
to: { opacity: 1, clipPath:
|
|
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
|
|
86
|
-
const t =
|
|
86
|
+
function $(s) {
|
|
87
|
+
const t = w({ ...s });
|
|
87
88
|
return { from: t.to, to: t.from };
|
|
88
89
|
}
|
|
89
|
-
function
|
|
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
|
|
100
|
-
return s.kind === "exit" ?
|
|
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
|
|
103
|
+
function z(s, t) {
|
|
104
|
+
var n, r;
|
|
122
105
|
const e = [];
|
|
123
|
-
let
|
|
106
|
+
let a = 0;
|
|
124
107
|
for (const i of t) {
|
|
125
|
-
const
|
|
126
|
-
if (!
|
|
127
|
-
const
|
|
128
|
-
i.trigger === "afterPrev" ?
|
|
129
|
-
const { from:
|
|
130
|
-
|
|
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
|
|
116
|
+
const u = c.animate(f, {
|
|
133
117
|
duration: i.durationMs,
|
|
134
|
-
delay:
|
|
135
|
-
|
|
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" &&
|
|
139
|
-
|
|
123
|
+
i.kind === "exit" && u.finished.then(() => {
|
|
124
|
+
c.style.visibility = "hidden";
|
|
140
125
|
}).catch(() => {
|
|
141
|
-
}), e.push(
|
|
126
|
+
}), e.push(u);
|
|
142
127
|
} catch {
|
|
143
|
-
Object.assign(
|
|
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
|
|
159
|
-
const e = s.dir ?? "",
|
|
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,
|
|
166
|
-
return t ? [{ transform:
|
|
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,
|
|
170
|
-
return t ? [{ transform:
|
|
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 =
|
|
174
|
-
return t ? [{ clipPath: r }, { clipPath:
|
|
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"),
|
|
178
|
-
return t ?
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
210
|
-
}))).then(() => {
|
|
301
|
+
return A(n, e.durationMs).then(() => {
|
|
211
302
|
s == null || s.remove();
|
|
212
303
|
});
|
|
213
304
|
}
|
|
214
|
-
function
|
|
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
|
|
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 ?
|
|
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 ?
|
|
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
|
|
277
|
-
if (
|
|
278
|
-
const
|
|
279
|
-
return this.idx =
|
|
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:
|
|
282
|
-
previous:
|
|
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 ?
|
|
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 =
|
|
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
|
|
345
|
-
for (let
|
|
346
|
-
this.text(
|
|
347
|
-
return
|
|
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),
|
|
353
|
-
return Number.isFinite(
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
|
370
|
-
|
|
371
|
-
}, this.
|
|
372
|
-
|
|
373
|
-
|
|
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 =
|
|
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
|
|
547
|
+
return v(this.presentation, this.presentation.slides[t], { textMode: this.textMode });
|
|
434
548
|
}
|
|
435
549
|
exportPng(t = 2, e = this.index) {
|
|
436
|
-
return
|
|
550
|
+
return C(this.presentation, this.presentation.slides[e], t);
|
|
437
551
|
}
|
|
438
552
|
paint(t) {
|
|
439
|
-
var
|
|
440
|
-
(
|
|
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
|
|
444
|
-
(!
|
|
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 =
|
|
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
|
|
458
|
-
e.style.visibility = t.has(
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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.
|
|
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.
|
|
53
|
+
"@web-ppt/core": "^0.3.0"
|
|
42
54
|
},
|
|
43
55
|
"publishConfig": {
|
|
44
56
|
"access": "public"
|