@sliiu/present-transition-fx 0.0.0-stage → 0.1.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.
Files changed (48) hide show
  1. package/COPYRIGHT +5 -0
  2. package/LICENSE +661 -0
  3. package/THIRD-PARTY-NOTICES.md +221 -0
  4. package/dist/cjs/2d.js +487 -0
  5. package/dist/cjs/3d.js +566 -0
  6. package/dist/cjs/geometry.js +141 -0
  7. package/dist/cjs/index.js +98 -0
  8. package/dist/cjs/morph-audit.js +603 -0
  9. package/dist/cjs/morph-match.js +103 -0
  10. package/dist/cjs/morph-path.js +649 -0
  11. package/dist/cjs/morph.js +223 -0
  12. package/dist/cjs/package.json +1 -0
  13. package/dist/cjs/registry.js +130 -0
  14. package/dist/cjs/snapshot.js +420 -0
  15. package/dist/cjs/twin.js +88 -0
  16. package/dist/cjs/types.js +2 -0
  17. package/dist/cjs/util.js +50 -0
  18. package/dist/cjs/webgl.js +271 -0
  19. package/dist/esm/2d.js +454 -0
  20. package/dist/esm/3d.js +538 -0
  21. package/dist/esm/geometry.js +127 -0
  22. package/dist/esm/index.js +6 -0
  23. package/dist/esm/morph-audit.js +558 -0
  24. package/dist/esm/morph-match.js +97 -0
  25. package/dist/esm/morph-path.js +636 -0
  26. package/dist/esm/morph.js +219 -0
  27. package/dist/esm/registry.js +91 -0
  28. package/dist/esm/snapshot.js +414 -0
  29. package/dist/esm/twin.js +84 -0
  30. package/dist/esm/types.js +1 -0
  31. package/dist/esm/util.js +43 -0
  32. package/dist/esm/webgl.js +231 -0
  33. package/dist/types/2d.d.ts +40 -0
  34. package/dist/types/3d.d.ts +38 -0
  35. package/dist/types/geometry.d.ts +38 -0
  36. package/dist/types/index.d.ts +7 -0
  37. package/dist/types/morph-audit.d.ts +127 -0
  38. package/dist/types/morph-match.d.ts +28 -0
  39. package/dist/types/morph-path.d.ts +37 -0
  40. package/dist/types/morph.d.ts +2 -0
  41. package/dist/types/registry.d.ts +6 -0
  42. package/dist/types/snapshot.d.ts +19 -0
  43. package/dist/types/twin.d.ts +2 -0
  44. package/dist/types/types.d.ts +13 -0
  45. package/dist/types/util.d.ts +12 -0
  46. package/dist/types/webgl.d.ts +5 -0
  47. package/package.json +26 -4
  48. package/README.md +0 -3
package/dist/esm/3d.js ADDED
@@ -0,0 +1,538 @@
1
+ import { peelFlatClip, rippleAt } from "./geometry.js";
2
+ import { dropTwins, mountTwin } from "./twin.js";
3
+ import { easing, ms, runAll, STD_EASE } from "./util.js";
4
+ function wrap3d(ctx) {
5
+ const parent = ctx.stage.parentElement;
6
+ if (!parent)
7
+ return { wrap: ctx.stage, cleanup: () => undefined };
8
+ const prevPerspective = parent.style.perspective;
9
+ const prevTransformStyle = parent.style.transformStyle;
10
+ parent.style.perspective = `${Math.max(ctx.stageSize.w, ctx.stageSize.h) * 1.5}px`;
11
+ parent.style.transformStyle = "preserve-3d";
12
+ ctx.stage.style.transformStyle = "preserve-3d";
13
+ ctx.ghost.style.transformStyle = "preserve-3d";
14
+ ctx.stage.style.backfaceVisibility = "hidden";
15
+ ctx.ghost.style.backfaceVisibility = "hidden";
16
+ return {
17
+ wrap: parent,
18
+ cleanup: () => {
19
+ parent.style.perspective = prevPerspective;
20
+ parent.style.transformStyle = prevTransformStyle;
21
+ ctx.stage.style.transformStyle = "";
22
+ ctx.ghost.style.transformStyle = "";
23
+ ctx.stage.style.backfaceVisibility = "";
24
+ ctx.ghost.style.backfaceVisibility = "";
25
+ },
26
+ };
27
+ }
28
+ export const cube = async (ctx) => {
29
+ const dir = String(ctx.params.dir ?? "left");
30
+ const { cleanup } = wrap3d(ctx);
31
+ const w = ctx.stageSize.w;
32
+ const h = ctx.stageSize.h;
33
+ const axis = dir === "left" || dir === "right" ? "Y" : "X";
34
+ const sign = dir === "left" || dir === "up" ? -1 : 1;
35
+ const half = (axis === "Y" ? w : h) / 2;
36
+ const sBase = ctx.stage.style.transform || "";
37
+ const gBase = ctx.ghost.style.transform || "";
38
+ ctx.stage.style.transformOrigin = "center center";
39
+ ctx.ghost.style.transformOrigin = "center center";
40
+ const a = ctx.stage.animate([
41
+ { transform: `translateZ(-${half}px) rotate${axis}(${sign * -90}deg) translateZ(${half}px) ${sBase}` },
42
+ { transform: `translateZ(-${half}px) rotate${axis}(0deg) translateZ(${half}px) ${sBase}` },
43
+ ], { duration: ms(ctx), easing: easing(ctx), fill: "forwards" });
44
+ const b = ctx.ghost.animate([
45
+ { transform: `translateZ(-${half}px) rotate${axis}(0deg) translateZ(${half}px) ${gBase}` },
46
+ { transform: `translateZ(-${half}px) rotate${axis}(${sign * 90}deg) translateZ(${half}px) ${gBase}` },
47
+ ], { duration: ms(ctx), easing: easing(ctx), fill: "forwards" });
48
+ await runAll([a, b], cleanup);
49
+ };
50
+ /** 三棱柱转 120°,和立方体的 90° 分开。左右绕 Y,上下绕 X。 */
51
+ export const prism = async (ctx) => {
52
+ const dir = String(ctx.params.dir ?? "left");
53
+ const { cleanup } = wrap3d(ctx);
54
+ const alongY = dir === "left" || dir === "right";
55
+ const half = (alongY ? ctx.stageSize.w : ctx.stageSize.h) / 2;
56
+ const axis = alongY ? "Y" : "X";
57
+ const sign = dir === "left" || dir === "up" ? -1 : 1;
58
+ const sBase = ctx.stage.style.transform || "";
59
+ const gBase = ctx.ghost.style.transform || "";
60
+ ctx.stage.style.transformOrigin = "center center";
61
+ ctx.ghost.style.transformOrigin = "center center";
62
+ const a = ctx.stage.animate([
63
+ { transform: `translateZ(-${half}px) rotate${axis}(${sign * -120}deg) translateZ(${half}px) ${sBase}` },
64
+ { transform: `translateZ(-${half}px) rotate${axis}(0deg) translateZ(${half}px) ${sBase}` },
65
+ ], { duration: ms(ctx), easing: easing(ctx), fill: "forwards" });
66
+ const b = ctx.ghost.animate([
67
+ { transform: `translateZ(-${half}px) rotate${axis}(0deg) translateZ(${half}px) ${gBase}` },
68
+ { transform: `translateZ(-${half}px) rotate${axis}(${sign * 120}deg) translateZ(${half}px) ${gBase}` },
69
+ ], { duration: ms(ctx), easing: easing(ctx), fill: "forwards" });
70
+ await runAll([a, b], cleanup);
71
+ };
72
+ /** 走进盒子:新页从内侧转出来,旧页往外让开。和立方体外表面对转分开。 */
73
+ export const box = async (ctx) => {
74
+ const dir = String(ctx.params.dir ?? "left");
75
+ const { cleanup } = wrap3d(ctx);
76
+ const alongY = dir === "left" || dir === "right";
77
+ const axis = alongY ? "Y" : "X";
78
+ const sign = dir === "left" || dir === "up" ? -1 : 1;
79
+ const half = (alongY ? ctx.stageSize.w : ctx.stageSize.h) / 2;
80
+ const sBase = ctx.stage.style.transform || "";
81
+ const gBase = ctx.ghost.style.transform || "";
82
+ ctx.stage.style.transformOrigin = "center center";
83
+ ctx.ghost.style.transformOrigin = "center center";
84
+ const a = ctx.stage.animate([
85
+ { transform: `translateZ(${half * 0.35}px) rotate${axis}(${sign * 70}deg) scale(0.82) ${sBase}`, opacity: 0.2 },
86
+ { transform: `translateZ(0px) rotate${axis}(0deg) scale(1) ${sBase}`, opacity: 1 },
87
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
88
+ const b = ctx.ghost.animate([
89
+ { transform: gBase, opacity: 1 },
90
+ { transform: `translateZ(-${half}px) rotate${axis}(${sign * -55}deg) scale(1.15) ${gBase}`, opacity: 0 },
91
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
92
+ await runAll([a, b], cleanup);
93
+ };
94
+ export const gallery = async (ctx) => {
95
+ const dir = String(ctx.params.dir ?? "right");
96
+ const { cleanup } = wrap3d(ctx);
97
+ const sign = dir === "left" ? -1 : 1;
98
+ const sBase = ctx.stage.style.transform || "";
99
+ const gBase = ctx.ghost.style.transform || "";
100
+ const a = ctx.stage.animate([
101
+ { transform: `translateX(${sign * ctx.stageSize.w}px) rotateY(${-sign * 90}deg) ${sBase}` },
102
+ { transform: `translateX(0px) rotateY(0deg) ${sBase}` },
103
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
104
+ const b = ctx.ghost.animate([
105
+ { transform: `translateX(0px) rotateY(0deg) ${gBase}` },
106
+ { transform: `translateX(${-sign * ctx.stageSize.w}px) rotateY(${sign * 90}deg) ${gBase}` },
107
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
108
+ await runAll([a, b], cleanup);
109
+ };
110
+ export const orbit = async (ctx) => {
111
+ const dir = String(ctx.params.dir ?? "left");
112
+ const { cleanup } = wrap3d(ctx);
113
+ const sign = dir === "left" ? -1 : 1;
114
+ const sBase = ctx.stage.style.transform || "";
115
+ const gBase = ctx.ghost.style.transform || "";
116
+ const a = ctx.stage.animate([
117
+ { transform: `translateZ(-200px) rotateY(${sign * 90}deg) translateZ(200px) ${sBase}`, opacity: 0 },
118
+ { transform: `translateZ(0) rotateY(0) translateZ(0) ${sBase}`, opacity: 1 },
119
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
120
+ const b = ctx.ghost.animate([
121
+ { transform: gBase, opacity: 1 },
122
+ { transform: `translateZ(-200px) rotateY(${-sign * 90}deg) ${gBase}`, opacity: 0 },
123
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
124
+ await runAll([a, b], cleanup);
125
+ };
126
+ export const conveyor = async (ctx) => {
127
+ const dir = String(ctx.params.dir ?? "right");
128
+ const { cleanup } = wrap3d(ctx);
129
+ const sign = dir === "left" ? -1 : 1;
130
+ const sBase = ctx.stage.style.transform || "";
131
+ const gBase = ctx.ghost.style.transform || "";
132
+ const a = ctx.stage.animate([
133
+ { transform: `translateX(${sign * ctx.stageSize.w}px) translateZ(-400px) ${sBase}` },
134
+ { transform: `translateX(0) translateZ(0) ${sBase}` },
135
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
136
+ const b = ctx.ghost.animate([
137
+ { transform: gBase },
138
+ { transform: `translateX(${-sign * ctx.stageSize.w}px) translateZ(-400px) ${gBase}` },
139
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
140
+ await runAll([a, b], cleanup);
141
+ };
142
+ function pageHalves(ctx, vert) {
143
+ const left = mountTwin(ctx.ghost, vert ? "inset(0 50% 0 0)" : "inset(0 0 50% 0)");
144
+ const right = mountTwin(ctx.ghost, vert ? "inset(0 0 0 50%)" : "inset(50% 0 0 0)");
145
+ const prev = ctx.ghost.style.visibility;
146
+ ctx.ghost.style.visibility = "hidden";
147
+ return { left, right, prev };
148
+ }
149
+ /** 旧页对开。垂直沿左右铰链转开,水平沿上下铰链转开。 */
150
+ export const doors = async (ctx) => {
151
+ const vert = String(ctx.params.orient ?? "vert") !== "horz";
152
+ const { cleanup } = wrap3d(ctx);
153
+ const { left, right, prev } = pageHalves(ctx, vert);
154
+ left.style.transformOrigin = vert ? "left center" : "center top";
155
+ right.style.transformOrigin = vert ? "right center" : "center bottom";
156
+ const openL = vert ? "perspective(900px) rotateY(-105deg)" : "perspective(900px) rotateX(105deg)";
157
+ const openR = vert ? "perspective(900px) rotateY(105deg)" : "perspective(900px) rotateX(-105deg)";
158
+ const dur = ms(ctx);
159
+ await runAll([
160
+ left.animate([{ transform: "none", opacity: 1 }, { transform: openL, opacity: 0 }], { duration: dur, easing: STD_EASE, fill: "forwards" }),
161
+ right.animate([{ transform: "none", opacity: 1 }, { transform: openR, opacity: 0 }], { duration: dur, easing: STD_EASE, fill: "forwards" }),
162
+ ], () => {
163
+ dropTwins([left, right], ctx.ghost, prev);
164
+ cleanup();
165
+ });
166
+ };
167
+ /** 窗:两扇平着滑开,不绕铰链转。和门分开。 */
168
+ export const window = async (ctx) => {
169
+ const vert = String(ctx.params.orient ?? "vert") !== "horz";
170
+ const { left, right, prev } = pageHalves(ctx, vert);
171
+ const offL = vert ? "translateX(-100%)" : "translateY(-100%)";
172
+ const offR = vert ? "translateX(100%)" : "translateY(100%)";
173
+ const baseL = left.style.transform || "";
174
+ const baseR = right.style.transform || "";
175
+ const dur = ms(ctx);
176
+ await runAll([
177
+ left.animate([{ transform: baseL }, { transform: `${offL} ${baseL}` }], { duration: dur, easing: STD_EASE, fill: "forwards" }),
178
+ right.animate([{ transform: baseR }, { transform: `${offR} ${baseR}` }], { duration: dur, easing: STD_EASE, fill: "forwards" }),
179
+ ], () => dropTwins([left, right], ctx.ghost, prev));
180
+ };
181
+ // 旧页绕一侧边缘翻起:towardRight 时铰链在右缘、左缘翻向右侧,否则镜像。
182
+ async function curlAway(ctx, towardRight) {
183
+ const { cleanup } = wrap3d(ctx);
184
+ const gBase = ctx.ghost.style.transform || "";
185
+ ctx.ghost.style.transformOrigin = towardRight ? "right center" : "left center";
186
+ const deg = towardRight ? -160 : 160;
187
+ const a = ctx.ghost.animate([
188
+ { transform: `perspective(1200px) rotateY(0deg) ${gBase}` },
189
+ { transform: `perspective(1200px) rotateY(${deg}deg) ${gBase}`, opacity: 0 },
190
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
191
+ await runAll([a], cleanup);
192
+ }
193
+ export const pageCurl = (ctx) => curlAway(ctx, true);
194
+ /** p15 pageCurlSingle:只掀起一角。整页卷曲走 pageCurl 的书页翻边。 */
195
+ export const pageCurlSingle = async (ctx) => {
196
+ const dir = String(ctx.params.dir ?? "left");
197
+ const fromRight = dir === "right";
198
+ const fromTop = dir === "up" || dir === "down";
199
+ const { cleanup } = wrap3d(ctx);
200
+ ctx.ghost.style.transformOrigin = `${fromRight ? "0%" : "100%"} ${fromTop ? "0%" : "100%"}`;
201
+ const gBase = ctx.ghost.style.transform || "";
202
+ const ry = fromRight ? -62 : 62;
203
+ const rx = fromTop ? -36 : 36;
204
+ const a = ctx.ghost.animate([
205
+ { transform: gBase, opacity: 1 },
206
+ { transform: `rotateX(${rx * 0.45}deg) rotateY(${ry * 0.45}deg) ${gBase}`, opacity: 1, offset: 0.4 },
207
+ { transform: `rotateX(${rx}deg) rotateY(${ry}deg) translateZ(80px) ${gBase}`, opacity: 0 },
208
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
209
+ await runAll([a], () => {
210
+ ctx.ghost.style.transformOrigin = "";
211
+ cleanup();
212
+ });
213
+ };
214
+ /** p15 wind:旧页切成细条,顺着风向依次吹出。向右 / 向下镜像。 */
215
+ export const wind = async (ctx) => {
216
+ const dir = String(ctx.params.dir ?? "left");
217
+ const vertical = dir === "up" || dir === "down";
218
+ const sign = dir === "right" || dir === "down" ? 1 : -1;
219
+ const count = 12;
220
+ const strips = [];
221
+ for (let i = 0; i < count; i++) {
222
+ const start = (i / count) * 100;
223
+ const end = ((count - i - 1) / count) * 100;
224
+ const clip = vertical ? `inset(0 ${end}% 0 ${start}%)` : `inset(${start}% 0 ${end}% 0)`;
225
+ strips.push(mountTwin(ctx.ghost, clip));
226
+ }
227
+ const prev = ctx.ghost.style.visibility;
228
+ ctx.ghost.style.visibility = "hidden";
229
+ const dur = ms(ctx);
230
+ const anims = strips.map((el, i) => {
231
+ const base = el.style.transform || "";
232
+ const shift = vertical
233
+ ? `translate(0, ${sign * 120}%) rotate(${sign * 16}deg)`
234
+ : `translate(${sign * 120}%, ${sign * -6}%) rotate(${sign * 18}deg)`;
235
+ return el.animate([
236
+ { transform: base, opacity: 1 },
237
+ { transform: `${shift} ${base}`, opacity: 0 },
238
+ ], { duration: dur * 0.62, delay: (i / count) * dur * 0.4, easing: easing(ctx), fill: "forwards" });
239
+ });
240
+ await runAll(anims, () => dropTwins(strips, ctx.ghost, prev));
241
+ };
242
+ /** p15 crush:旧页朝选定的边压成一条线。 */
243
+ export const crush = async (ctx) => {
244
+ const dir = String(ctx.params.dir ?? "left");
245
+ const vertical = dir === "up" || dir === "down";
246
+ ctx.ghost.style.transformOrigin = vertical
247
+ ? (dir === "down" ? "center bottom" : "center top")
248
+ : (dir === "right" ? "right center" : "left center");
249
+ const gBase = ctx.ghost.style.transform || "";
250
+ const flat = vertical ? "scaleX(1.12) scaleY(0.02)" : "scaleX(0.02) scaleY(1.12)";
251
+ const a = ctx.ghost.animate([
252
+ { transform: gBase, opacity: 1 },
253
+ { transform: `${flat} ${gBase}`, opacity: 0 },
254
+ ], { duration: ms(ctx), easing: easing(ctx), fill: "forwards" });
255
+ await runAll([a], () => {
256
+ ctx.ghost.style.transformOrigin = "";
257
+ });
258
+ };
259
+ // [MS-PPTX] p15:prstTrans prst="airplane":旧页原地折成纸飞机(两个前角折向中线成机头,
260
+ // 右缘收成机尾),再朝左上飞出画面,新页一直静止在下面。invX(params.dir="right")镜像成朝右上。
261
+ // clip-path 各帧点数必须相同,WAAPI 才能插值:左边两个角都收到机头,右缘三个点收成尾翼缺口。
262
+ const AIRPLANE_SHEET = [[0, 0], [100, 0], [100, 40], [100, 50], [100, 60], [100, 100], [0, 100]];
263
+ const AIRPLANE_PLANE = [[4, 49], [96, 8], [84, 42], [92, 50], [84, 58], [96, 92], [4, 51]];
264
+ function airplanePolygon(pts, mirror) {
265
+ return `polygon(${pts.map(([x, y]) => `${mirror ? 100 - x : x}% ${y}%`).join(", ")})`;
266
+ }
267
+ export const airplane = async (ctx) => {
268
+ const ghost = ctx.ghost;
269
+ const doc = ghost.ownerDocument;
270
+ const mirror = String(ctx.params.dir ?? "left") === "right";
271
+ const sx = mirror ? -1 : 1;
272
+ const gBase = ghost.style.transform || "";
273
+ const tf = (x, y, deg, s) => `translate(${-sx * x}%, ${y}%) rotate(${sx * deg}deg) scale(${s}) ${gBase}`.trim();
274
+ const sheet = airplanePolygon(AIRPLANE_SHEET, mirror);
275
+ const plane = airplanePolygon(AIRPLANE_PLANE, mirror);
276
+ // clip-path 会连 filter 一起裁掉,投影只能挂在外层;外层 0×0 + overflow 可见,不改变 ghost 的定位基准。
277
+ const parent = ghost.parentElement;
278
+ const shadowWrap = doc.createElement("div");
279
+ shadowWrap.style.cssText = "position:absolute;left:0;top:0;width:0;height:0;overflow:visible;pointer-events:none";
280
+ shadowWrap.style.zIndex = ghost.style.zIndex || "2";
281
+ if (parent) {
282
+ parent.insertBefore(shadowWrap, ghost);
283
+ shadowWrap.appendChild(ghost);
284
+ }
285
+ // 折痕:机身中线一道暗线,上翼略亮、下翼略暗;跟随 ghost 一起被裁成飞机形。
286
+ const crease = doc.createElement("div");
287
+ crease.style.cssText = [
288
+ "position:absolute",
289
+ "inset:0",
290
+ "pointer-events:none",
291
+ "opacity:0",
292
+ "background:linear-gradient(to bottom, rgba(255,255,255,0.22) 0%, rgba(255,255,255,0) 44%, rgba(0,0,0,0.5) 50%, rgba(0,0,0,0) 56%, rgba(0,0,0,0.28) 100%)",
293
+ ].join(";");
294
+ ghost.appendChild(crease);
295
+ const prevOrigin = ghost.style.transformOrigin;
296
+ const prevClip = ghost.style.clipPath;
297
+ ghost.style.transformOrigin = "50% 50%";
298
+ const dur = ms(ctx);
299
+ const fold = "cubic-bezier(0.45, 0, 0.55, 1)";
300
+ const a = ghost.animate([
301
+ { offset: 0, clipPath: sheet, transform: tf(0, 0, 0, 1), easing: fold },
302
+ { offset: 0.1, clipPath: sheet, transform: tf(0, 0, 0, 0.94), easing: fold },
303
+ { offset: 0.45, clipPath: plane, transform: tf(0, 0, 0, 0.5), easing: fold },
304
+ // 起飞前先往反方向压一下,再加速爬升。
305
+ { offset: 0.55, clipPath: plane, transform: tf(-6, 4, 4, 0.5), easing: "cubic-bezier(0.55, 0, 0.9, 0.45)" },
306
+ { offset: 1, clipPath: plane, transform: tf(135, -110, 24, 0.34) },
307
+ ], { duration: dur, fill: "forwards" });
308
+ const b = crease.animate([
309
+ { offset: 0, opacity: 0 },
310
+ { offset: 0.1, opacity: 0 },
311
+ { offset: 0.45, opacity: 1 },
312
+ { offset: 1, opacity: 1 },
313
+ ], { duration: dur, fill: "forwards" });
314
+ const c = shadowWrap.animate([
315
+ { offset: 0, filter: "drop-shadow(0 0 0 rgba(0,0,0,0))" },
316
+ { offset: 0.45, filter: "drop-shadow(0 12px 16px rgba(0,0,0,0.45))" },
317
+ { offset: 1, filter: "drop-shadow(0 24px 22px rgba(0,0,0,0.35))" },
318
+ ], { duration: dur, fill: "forwards" });
319
+ await runAll([a, b, c], () => {
320
+ crease.remove();
321
+ ghost.style.transformOrigin = prevOrigin;
322
+ ghost.style.clipPath = prevClip;
323
+ if (parent) {
324
+ parent.insertBefore(ghost, shadowWrap);
325
+ shadowWrap.remove();
326
+ }
327
+ });
328
+ };
329
+ /** 对折两次再飞走。向右时折痕在右缘。和翻页的单次卷起分开。 */
330
+ export const origami = async (ctx) => {
331
+ const mirror = String(ctx.params.dir ?? "left") === "right";
332
+ const gBase = ctx.ghost.style.transform || "";
333
+ const origin = ctx.ghost.style.transformOrigin;
334
+ ctx.ghost.style.transformOrigin = mirror ? "right center" : "left center";
335
+ const fly = mirror ? -130 : 130;
336
+ const a = ctx.ghost.animate([
337
+ { offset: 0, transform: `scaleX(1) ${gBase}`, opacity: 1 },
338
+ { offset: 0.42, transform: `scaleX(0.06) ${gBase}`, opacity: 1 },
339
+ { offset: 1, transform: `scaleX(0.06) translate(${fly}%, -36%) rotate(${mirror ? -18 : 18}deg) ${gBase}`, opacity: 0 },
340
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
341
+ await runAll([a], () => {
342
+ ctx.ghost.style.transformOrigin = origin;
343
+ });
344
+ };
345
+ export const flythrough = async (ctx) => {
346
+ const out = String(ctx.params.dir ?? "in") === "out";
347
+ const { cleanup } = wrap3d(ctx);
348
+ const sBase = ctx.stage.style.transform || "";
349
+ const gBase = ctx.ghost.style.transform || "";
350
+ const anims = out
351
+ ? [
352
+ ctx.stage.animate([
353
+ { transform: `${sBase} translateZ(-1400px) scale(0.35)`, opacity: 0 },
354
+ { transform: sBase, opacity: 1 },
355
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" }),
356
+ ctx.ghost.animate([{ opacity: 1 }, { opacity: 0 }], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" }),
357
+ ]
358
+ : [
359
+ ctx.ghost.animate([
360
+ { transform: gBase, opacity: 1 },
361
+ { transform: `${gBase} translateZ(900px) scale(2.6)`, opacity: 0 },
362
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" }),
363
+ ];
364
+ await runAll(anims, cleanup);
365
+ };
366
+ export const rotate = async (ctx) => {
367
+ const dir = String(ctx.params.dir ?? "left");
368
+ const { cleanup } = wrap3d(ctx);
369
+ const axis = dir === "left" || dir === "right" ? "Y" : "X";
370
+ const sign = dir === "left" || dir === "up" ? -1 : 1;
371
+ const sBase = ctx.stage.style.transform || "";
372
+ const gBase = ctx.ghost.style.transform || "";
373
+ const a = ctx.stage.animate([
374
+ { transform: `${sBase} rotate${axis}(${sign * 180}deg)`, opacity: 0 },
375
+ { transform: sBase, opacity: 1 },
376
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
377
+ const b = ctx.ghost.animate([{ transform: gBase, opacity: 1 }, { transform: `${gBase} rotate${axis}(${-sign * 180}deg)`, opacity: 0 }], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
378
+ await runAll([a, b], cleanup);
379
+ };
380
+ /** 绕画面外侧的轴转过去,像摩天轮的一格,不是原地翻面。 */
381
+ export const ferris = async (ctx) => {
382
+ const toRight = String(ctx.params.dir ?? "left") === "right";
383
+ const { cleanup } = wrap3d(ctx);
384
+ const origin = toRight ? "120% 50%" : "-20% 50%";
385
+ const sign = toRight ? 1 : -1;
386
+ const prevS = ctx.stage.style.transformOrigin;
387
+ const prevG = ctx.ghost.style.transformOrigin;
388
+ ctx.stage.style.transformOrigin = origin;
389
+ ctx.ghost.style.transformOrigin = origin;
390
+ const sBase = ctx.stage.style.transform || "";
391
+ const gBase = ctx.ghost.style.transform || "";
392
+ const a = ctx.stage.animate([
393
+ { transform: `rotate(${sign * -180}deg) ${sBase}` },
394
+ { transform: `rotate(0deg) ${sBase}` },
395
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
396
+ const b = ctx.ghost.animate([
397
+ { transform: `rotate(0deg) ${gBase}` },
398
+ { transform: `rotate(${sign * 180}deg) ${gBase}` },
399
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
400
+ await runAll([a, b], () => {
401
+ ctx.stage.style.transformOrigin = prevS;
402
+ ctx.ghost.style.transformOrigin = prevG;
403
+ cleanup();
404
+ });
405
+ };
406
+ // [MS-PPTX] p15:prstTrans prst="peelOff":默认从右下角朝左揭起,invX(params.dir="right")朝右。
407
+ export const peelOff = async (ctx) => {
408
+ const towardRight = String(ctx.params.dir ?? "left") === "right";
409
+ const { cleanup } = wrap3d(ctx);
410
+ const prevOrigin = ctx.ghost.style.transformOrigin;
411
+ const gBase = ctx.ghost.style.transform || "";
412
+ ctx.ghost.style.transformOrigin = towardRight ? "100% 0%" : "0% 0%";
413
+ const lift = towardRight ? 58 : -58;
414
+ const a = ctx.ghost.animate([
415
+ { clipPath: peelFlatClip(towardRight, 0), transform: gBase, opacity: 1 },
416
+ { clipPath: peelFlatClip(towardRight, 0.45), transform: `perspective(1100px) rotate(${lift * 0.35}deg) ${gBase}`, opacity: 1, offset: 0.45 },
417
+ { clipPath: peelFlatClip(towardRight, 1), transform: `perspective(1100px) rotateX(62deg) rotate(${lift}deg) ${gBase}`, opacity: 0 },
418
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
419
+ await runAll([a], () => {
420
+ ctx.ghost.style.clipPath = "";
421
+ ctx.ghost.style.transformOrigin = prevOrigin;
422
+ cleanup();
423
+ });
424
+ };
425
+ /** 卡片翻面:转到 90° 时只看见棱,没有旋转那种半透明交叠。 */
426
+ export const flip = async (ctx) => {
427
+ const dir = String(ctx.params.dir ?? "left");
428
+ const { cleanup } = wrap3d(ctx);
429
+ const axis = dir === "up" || dir === "down" ? "X" : "Y";
430
+ const sign = dir === "right" || dir === "down" ? 1 : -1;
431
+ const sBase = ctx.stage.style.transform || "";
432
+ const gBase = ctx.ghost.style.transform || "";
433
+ ctx.stage.style.backfaceVisibility = "hidden";
434
+ ctx.ghost.style.backfaceVisibility = "hidden";
435
+ const a = ctx.stage.animate([
436
+ { transform: `${sBase} rotate${axis}(${sign * -180}deg)` },
437
+ { transform: sBase },
438
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
439
+ const b = ctx.ghost.animate([
440
+ { transform: gBase },
441
+ { transform: `${gBase} rotate${axis}(${sign * 180}deg)` },
442
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
443
+ await runAll([a, b], cleanup);
444
+ };
445
+ /** 两页左右错身换位,带一点倾斜。 */
446
+ export const switchFx = async (ctx) => {
447
+ const toLeft = String(ctx.params.dir ?? "left") !== "right";
448
+ const sign = toLeft ? -1 : 1;
449
+ const w = ctx.stageSize.w;
450
+ const sBase = ctx.stage.style.transform || "";
451
+ const gBase = ctx.ghost.style.transform || "";
452
+ const a = ctx.stage.animate([
453
+ { transform: `translateX(${sign * w}px) rotate(${sign * -7}deg) ${sBase}` },
454
+ { transform: sBase },
455
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
456
+ const b = ctx.ghost.animate([
457
+ { transform: gBase },
458
+ { transform: `translateX(${-sign * w}px) rotate(${sign * 7}deg) ${gBase}` },
459
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
460
+ await runAll([a, b]);
461
+ };
462
+ export const drape = async (ctx) => {
463
+ const toRight = String(ctx.params.dir ?? "left") === "right";
464
+ const sign = toRight ? 1 : -1;
465
+ const sBase = ctx.stage.style.transform || "";
466
+ const gBase = ctx.ghost.style.transform || "";
467
+ const ease = "cubic-bezier(0.16, 1, 0.3, 1)";
468
+ const a = ctx.stage.animate([{ transform: `${sBase} translateY(-100%)` }, { transform: sBase }], { duration: ms(ctx), easing: ease, fill: "forwards" });
469
+ const b = ctx.ghost.animate([
470
+ { transform: gBase, opacity: 1 },
471
+ { transform: `${gBase} translate(${sign * 28}%, 110%) rotate(${sign * 11}deg)`, opacity: 0 },
472
+ ], { duration: ms(ctx), easing: ease, fill: "forwards" });
473
+ await runAll([a, b]);
474
+ };
475
+ /** 帘幕:多条竖褶从中缝向两边收走。 */
476
+ export const curtains = async (ctx) => {
477
+ const count = 8;
478
+ const folds = [];
479
+ for (let i = 0; i < count; i++) {
480
+ const left = (i / count) * 100;
481
+ const right = ((count - i - 1) / count) * 100;
482
+ folds.push(mountTwin(ctx.ghost, `inset(0 ${right}% 0 ${left}%)`));
483
+ }
484
+ const prev = ctx.ghost.style.visibility;
485
+ ctx.ghost.style.visibility = "hidden";
486
+ const dur = ms(ctx);
487
+ const anims = folds.map((el, i) => {
488
+ const toLeft = i < count / 2;
489
+ const base = el.style.transform || "";
490
+ return el.animate([
491
+ { transform: base, opacity: 1 },
492
+ { transform: `translateX(${toLeft ? -110 : 110}%) scaleX(0.35) ${base}`, opacity: 0.15 },
493
+ ], { duration: dur, easing: STD_EASE, fill: "forwards", delay: Math.abs(i - (count - 1) / 2) * dur * 0.04 });
494
+ });
495
+ await runAll(anims, () => dropTwins(folds, ctx.ghost, prev));
496
+ };
497
+ export const ripple = async (ctx) => {
498
+ const at = rippleAt(String(ctx.params.dir ?? ""));
499
+ const a = ctx.ghost.animate([
500
+ { clipPath: `circle(160% at ${at})`, opacity: 1 },
501
+ { clipPath: `circle(0% at ${at})`, opacity: 0 },
502
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
503
+ await runAll([a], () => {
504
+ ctx.ghost.style.clipPath = "";
505
+ });
506
+ };
507
+ export const warp = async (ctx) => {
508
+ const out = String(ctx.params.dir ?? "in") === "out";
509
+ const gBase = ctx.ghost.style.transform || "";
510
+ const sBase = ctx.stage.style.transform || "";
511
+ if (out) {
512
+ const a = ctx.stage.animate([
513
+ { transform: `scale(0.08) skew(22deg) ${sBase}`, opacity: 0 },
514
+ { transform: sBase, opacity: 1 },
515
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
516
+ const b = ctx.ghost.animate([{ opacity: 1 }, { opacity: 0 }], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
517
+ await runAll([a, b]);
518
+ return;
519
+ }
520
+ const a = ctx.ghost.animate([
521
+ { transform: gBase, opacity: 1 },
522
+ { transform: `scale(0.08) skew(-22deg) ${gBase}`, opacity: 0 },
523
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
524
+ await runAll([a]);
525
+ };
526
+ export const vortex = async (ctx) => {
527
+ const dir = String(ctx.params.dir ?? "left");
528
+ const gBase = ctx.ghost.style.transform || "";
529
+ const clockwise = dir === "right" || dir === "down";
530
+ const spin = clockwise ? 540 : -540;
531
+ const vertical = dir === "up" || dir === "down";
532
+ const turn = vertical ? `rotateX(${spin}deg)` : `rotate(${spin}deg)`;
533
+ const a = ctx.ghost.animate([
534
+ { transform: gBase, opacity: 1 },
535
+ { transform: `scale(0) ${turn} ${gBase}`, opacity: 0 },
536
+ ], { duration: ms(ctx), easing: STD_EASE, fill: "forwards" });
537
+ await runAll([a]);
538
+ };
@@ -0,0 +1,127 @@
1
+ // 切换效果里可以脱离 DOM 核对的几何:分割的开合、轮子的轮辐、条纹的角、涟漪的圆心、剥离的折角。
2
+ // 播放函数只负责把这些字符串套到 clip-path / mask 上。
3
+ /** orient=vert 左右分开,horz 上下分开。dir=in 从两侧收到中间,其余从中间向外展开。 */
4
+ export function splitMotion(orient, dir) {
5
+ const vert = orient === "vert";
6
+ if (dir === "in")
7
+ return { kind: "meet", axis: vert ? "x" : "y" };
8
+ return { kind: "expand", clip: vert ? "inset(0 50% 0 50%)" : "inset(50% 0 50% 0)" };
9
+ }
10
+ /** 面板上的四档(中央向左右展开 / 左右向中央收缩 / …)和 OOXML orient+dir 互转。 */
11
+ export function splitToken(orient, dir) {
12
+ return `${orient === "vert" ? "vert" : "horz"}${dir === "in" ? "In" : "Out"}`;
13
+ }
14
+ export function splitParamsFromToken(token) {
15
+ const vert = token.startsWith("vert");
16
+ const inward = token.endsWith("In");
17
+ return { orient: vert ? "vert" : "horz", dir: inward ? "in" : "out" };
18
+ }
19
+ /** OOXML CT_WheelTransition 的 spokes。缺省 4。界面提供 1/2/3/4/8,其它 1–8 的整数照播。 */
20
+ export function wheelSpokes(pattern) {
21
+ const n = Number(pattern);
22
+ if (!Number.isFinite(n))
23
+ return 4;
24
+ const rounded = Math.round(n);
25
+ if (rounded < 1 || rounded > 8)
26
+ return 4;
27
+ return rounded;
28
+ }
29
+ /** 固定点数的轮辐多边形,portion 0 收成射线,1 铺满画面。reverse 往反方向张开。 */
30
+ export function wheelPolygon(spokes, portion, reverse) {
31
+ const n = Math.max(1, Math.min(8, Math.round(spokes)));
32
+ const steps = 6;
33
+ const p = Math.min(1, Math.max(0, portion));
34
+ const spin = reverse ? -1 : 1;
35
+ const pts = [];
36
+ for (let s = 0; s < n; s++) {
37
+ const startAng = spin * (s / n) * Math.PI * 2 - Math.PI / 2;
38
+ const sweep = spin * ((p * Math.PI * 2) / n);
39
+ pts.push("50% 50%");
40
+ for (let i = 0; i <= steps; i++) {
41
+ const ang = startAng + sweep * (i / steps);
42
+ const x = 50 + Math.cos(ang) * 90;
43
+ const y = 50 + Math.sin(ang) * 90;
44
+ pts.push(`${x.toFixed(2)}% ${y.toFixed(2)}%`);
45
+ }
46
+ }
47
+ return `polygon(${pts.join(",")})`;
48
+ }
49
+ const STRIP_ORIGIN = {
50
+ topLeft: [100, 100],
51
+ topRight: [0, 100],
52
+ bottomLeft: [100, 0],
53
+ bottomRight: [0, 0],
54
+ };
55
+ /** 条纹沿 dir 扫过。dir 是运动方向(topLeft = 朝左上,即从右下进来)。三点同点数,方便插值。 */
56
+ export function stripsClips(dir) {
57
+ const [ox, oy] = STRIP_ORIGIN[dir] ?? STRIP_ORIGIN.bottomRight;
58
+ const sx = ox === 0 ? 1 : -1;
59
+ const sy = oy === 0 ? 1 : -1;
60
+ const at = (dx, dy) => {
61
+ const x = ox + sx * dx;
62
+ const y = oy + sy * dy;
63
+ return `polygon(${x}% ${y}%, ${x}% ${y}%, ${ox}% ${oy}%, ${ox}% ${oy}%)`;
64
+ };
65
+ const end = "polygon(-15% -15%, 115% -15%, 115% 115%, -15% 115%)";
66
+ return [at(0, 0), at(70, 35), end];
67
+ }
68
+ /** 涟漪收缩的圆心。角方向用面板的 topLeft / topRight / bottomLeft / bottomRight。 */
69
+ export function rippleAt(dir) {
70
+ switch (dir) {
71
+ case "topLeft":
72
+ return "0% 0%";
73
+ case "topRight":
74
+ return "100% 0%";
75
+ case "bottomLeft":
76
+ return "0% 100%";
77
+ case "bottomRight":
78
+ return "100% 100%";
79
+ default:
80
+ return "50% 50%";
81
+ }
82
+ }
83
+ const DIAMOND_TILE = `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28"><polygon points="14,2 26,14 14,26 2,14" fill="black"/></svg>`;
84
+ const HEX_TILE = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="28" viewBox="0 0 32 28"><polygon points="8,2 24,2 30,14 24,26 8,26 2,14" fill="black"/></svg>`;
85
+ const HONEY_TILE = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="36" viewBox="0 0 40 36"><polygon points="12,3 28,3 36,18 28,33 12,33 4,18" fill="black"/></svg>`;
86
+ /** 闪耀的菱形 / 六边形,以及蜂巢的疏六边形。mask 里黑色是露出的部分。 */
87
+ export function cellMask(kind) {
88
+ const svg = kind === "honeycomb" ? HONEY_TILE : kind === "hexagon" ? HEX_TILE : DIAMOND_TILE;
89
+ return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
90
+ }
91
+ /** 面板上「方向 · 图案」和 params.dir + params.pattern 互转。缺省向左、菱形。 */
92
+ export function glitterToken(dir, pattern) {
93
+ const d = dir === "right" || dir === "up" || dir === "down" ? String(dir) : "left";
94
+ const p = pattern === "hexagon" ? "hexagon" : "diamond";
95
+ return `${d}:${p}`;
96
+ }
97
+ export function glitterParamsFromToken(token) {
98
+ const [rawDir, rawPattern] = token.split(":");
99
+ const dir = rawDir === "right" || rawDir === "up" || rawDir === "down" ? rawDir : "left";
100
+ const pattern = rawPattern === "hexagon" ? "hexagon" : "diamond";
101
+ return { dir, pattern };
102
+ }
103
+ /** 闪耀扫过的起点和终点。向左从左缘扫到右缘,其余镜像。 */
104
+ export function glitterSweep(dir) {
105
+ switch (dir) {
106
+ case "right":
107
+ return { from: "120% 50%", to: "-20% 50%" };
108
+ case "up":
109
+ return { from: "50% 120%", to: "50% -20%" };
110
+ case "down":
111
+ return { from: "50% -20%", to: "50% 120%" };
112
+ default:
113
+ return { from: "-20% 50%", to: "120% 50%" };
114
+ }
115
+ }
116
+ /** 剥离后仍平贴的那一块。towardRight 从左下角往右揭,否则从右下角往左揭。amount 0 整页,1 几乎揭完。 */
117
+ export function peelFlatClip(towardRight, amount) {
118
+ const a = Math.min(1, Math.max(0, amount));
119
+ if (towardRight) {
120
+ const x = (a * 78).toFixed(1);
121
+ const y = (100 - a * 78).toFixed(1);
122
+ return `polygon(0% 0%, 100% 0%, 100% 100%, ${x}% ${y}%)`;
123
+ }
124
+ const x = (100 - a * 78).toFixed(1);
125
+ const y = (100 - a * 78).toFixed(1);
126
+ return `polygon(0% 0%, 100% 0%, ${x}% ${y}%, 0% 100%)`;
127
+ }
@@ -0,0 +1,6 @@
1
+ export { alignClosedPolylines, ellipsePathFromSize, flattenPathToPoints, makeMorphPathTween, morphPathCompatible, morphPathSignature, linePathFromPoints, parsePathCommands, pathToAbsolute, polygonPointsToPath, rectPathFromSize, } from "./morph-path.js";
2
+ export { auditMorphDeck, auditMorphSlides, averageMorphIssuesPerPair, averageMorphReadinessPercent, averagePathMorphCoveragePercent, collectMorphCandidatesFromSlide, collectUnmatchedMorphShapesInDeck, countMorphPairsClean, countMorphPairsWithIssues, exportMorphAuditText, exportMorphDeckAuditText, exportMorphDeckCompactText, exportMorphDeckFilteredCompactText, exportMorphDeckIssuesCompactText, exportMorphDeckReadinessText, exportMorphIssuesCompactText, exportMorphPairCoverageText, exportMorphPairCompactText, exportMorphReadinessText, exportMorphUnmatchedShapesText, filterMorphDeckEntries, findFirstIssueMorphPair, findHighestPathCoverageMorphPair, findLowestPathCoverageMorphPair, findMostIssueMorphPair, findStrongestMorphPair, findWeakestMorphPair, formatMorphDeckChromeLines, formatMorphDeckReadinessLine, formatMorphDeckStatsLine, formatMorphLowestPathLine, formatMorphMatchReasonBreakdown, formatMorphPairCoverageBreakdownLine, formatMorphReadinessLine, formatMorphReadinessScoreLine, formatMorphReadinessSummary, morphCandidateFromShape, morphIssueCount, morphPairsWithIssues, morphReadinessPercent, countMorphDeckIssues, pathMorphCoveragePercent, summarizeMorphMatchReasons, } from "./morph-audit.js";
3
+ export { matchMorphShapePairs, morphGeometryHash, morphMatchReason, morphSimilarity, } from "./morph-match.js";
4
+ export * from "./types.js";
5
+ export { cellMask, glitterParamsFromToken, glitterSweep, glitterToken, peelFlatClip, rippleAt, splitMotion, splitParamsFromToken, splitToken, stripsClips, wheelPolygon, wheelSpokes, } from "./geometry.js";
6
+ export { getTransition, listTransitions, registerTransition, runTransition, } from "./registry.js";