@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.
- package/COPYRIGHT +5 -0
- package/LICENSE +661 -0
- package/THIRD-PARTY-NOTICES.md +221 -0
- package/dist/cjs/2d.js +487 -0
- package/dist/cjs/3d.js +566 -0
- package/dist/cjs/geometry.js +141 -0
- package/dist/cjs/index.js +98 -0
- package/dist/cjs/morph-audit.js +603 -0
- package/dist/cjs/morph-match.js +103 -0
- package/dist/cjs/morph-path.js +649 -0
- package/dist/cjs/morph.js +223 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/registry.js +130 -0
- package/dist/cjs/snapshot.js +420 -0
- package/dist/cjs/twin.js +88 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/util.js +50 -0
- package/dist/cjs/webgl.js +271 -0
- package/dist/esm/2d.js +454 -0
- package/dist/esm/3d.js +538 -0
- package/dist/esm/geometry.js +127 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/morph-audit.js +558 -0
- package/dist/esm/morph-match.js +97 -0
- package/dist/esm/morph-path.js +636 -0
- package/dist/esm/morph.js +219 -0
- package/dist/esm/registry.js +91 -0
- package/dist/esm/snapshot.js +414 -0
- package/dist/esm/twin.js +84 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/util.js +43 -0
- package/dist/esm/webgl.js +231 -0
- package/dist/types/2d.d.ts +40 -0
- package/dist/types/3d.d.ts +38 -0
- package/dist/types/geometry.d.ts +38 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/morph-audit.d.ts +127 -0
- package/dist/types/morph-match.d.ts +28 -0
- package/dist/types/morph-path.d.ts +37 -0
- package/dist/types/morph.d.ts +2 -0
- package/dist/types/registry.d.ts +6 -0
- package/dist/types/snapshot.d.ts +19 -0
- package/dist/types/twin.d.ts +2 -0
- package/dist/types/types.d.ts +13 -0
- package/dist/types/util.d.ts +12 -0
- package/dist/types/webgl.d.ts +5 -0
- package/package.json +26 -4
- package/README.md +0 -3
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { makeMorphPathTween, ellipsePathFromSize, linePathFromPoints, polygonPointsToPath, rectPathFromSize } from "./morph-path.js";
|
|
2
|
+
import { matchMorphShapePairs, morphGeometryHash, } from "./morph-match.js";
|
|
3
|
+
import { ms, runAll, STD_EASE } from "./util.js";
|
|
4
|
+
const PATH_RE = /^path|polygon|polyline$/;
|
|
5
|
+
function readShapeId(el) {
|
|
6
|
+
const id = el.getAttribute("data-shape-id") ?? el.getAttribute("data-id");
|
|
7
|
+
if (!id || id.startsWith("__"))
|
|
8
|
+
return null;
|
|
9
|
+
return id;
|
|
10
|
+
}
|
|
11
|
+
function readShapeName(el, id) {
|
|
12
|
+
return el.getAttribute("data-shape-name") ?? el.getAttribute("data-name") ?? id;
|
|
13
|
+
}
|
|
14
|
+
// Nested shapes (grpSp children, the parser's `_base`/`_text` halves) ride on their outer shape's
|
|
15
|
+
// FLIP transform; matching them too would stack a second transform on top.
|
|
16
|
+
function hasShapeAncestor(el, root) {
|
|
17
|
+
for (let p = el.parentElement; p && p !== root; p = p.parentElement) {
|
|
18
|
+
if (readShapeId(p))
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
function collectMatchable(root) {
|
|
24
|
+
const out = [];
|
|
25
|
+
root.querySelectorAll("[data-shape-id], [data-id]").forEach((el) => {
|
|
26
|
+
const id = readShapeId(el);
|
|
27
|
+
if (!id || hasShapeAncestor(el, root))
|
|
28
|
+
return;
|
|
29
|
+
const name = readShapeName(el, id);
|
|
30
|
+
const rect = el.getBoundingClientRect();
|
|
31
|
+
if (rect.width < 1 && rect.height < 1)
|
|
32
|
+
return;
|
|
33
|
+
let pathD;
|
|
34
|
+
const tag = el.tagName.toLowerCase();
|
|
35
|
+
const pathEl = tag === "path"
|
|
36
|
+
? el
|
|
37
|
+
: el.querySelector("path");
|
|
38
|
+
if (pathEl) {
|
|
39
|
+
pathD = pathEl.getAttribute("d") ?? undefined;
|
|
40
|
+
}
|
|
41
|
+
else if (PATH_RE.test(tag)) {
|
|
42
|
+
pathD = polygonPointsToPath(el.getAttribute("points") ?? "", tag === "polygon");
|
|
43
|
+
}
|
|
44
|
+
else if (tag === "ellipse" || tag === "circle") {
|
|
45
|
+
pathD = ellipsePathFromSize(rect.width, rect.height);
|
|
46
|
+
}
|
|
47
|
+
else if (tag === "rect") {
|
|
48
|
+
pathD = rectPathFromSize(rect.width, rect.height);
|
|
49
|
+
}
|
|
50
|
+
else if (tag === "line") {
|
|
51
|
+
const lineEl = el;
|
|
52
|
+
const x1 = Number(lineEl.getAttribute("x1") ?? 0);
|
|
53
|
+
const y1 = Number(lineEl.getAttribute("y1") ?? 0);
|
|
54
|
+
const x2 = Number(lineEl.getAttribute("x2") ?? rect.width);
|
|
55
|
+
const y2 = Number(lineEl.getAttribute("y2") ?? rect.height);
|
|
56
|
+
pathD = linePathFromPoints(x1, y1, x2, y2);
|
|
57
|
+
}
|
|
58
|
+
const candidate = {
|
|
59
|
+
id,
|
|
60
|
+
name,
|
|
61
|
+
tag,
|
|
62
|
+
rect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
|
|
63
|
+
pathD,
|
|
64
|
+
placeholderType: el.getAttribute("data-placeholder-type") ?? undefined,
|
|
65
|
+
};
|
|
66
|
+
const geomHash = morphGeometryHash(candidate);
|
|
67
|
+
out.push({ id, name, geomHash, rect, pathD, el: el });
|
|
68
|
+
});
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
function toCandidate(m) {
|
|
72
|
+
return {
|
|
73
|
+
id: m.id,
|
|
74
|
+
name: m.name,
|
|
75
|
+
tag: m.geomHash.split("|")[0] ?? "g",
|
|
76
|
+
rect: {
|
|
77
|
+
left: m.rect.left,
|
|
78
|
+
top: m.rect.top,
|
|
79
|
+
width: m.rect.width,
|
|
80
|
+
height: m.rect.height,
|
|
81
|
+
},
|
|
82
|
+
pathD: m.pathD,
|
|
83
|
+
placeholderType: m.el.getAttribute("data-placeholder-type") ?? undefined,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function pairMatchable(ghost, stage) {
|
|
87
|
+
const ghostCands = ghost.map(toCandidate);
|
|
88
|
+
const stageCands = stage.map(toCandidate);
|
|
89
|
+
const out = new Map();
|
|
90
|
+
for (const { ghost: g, stage: s } of matchMorphShapePairs(ghostCands, stageCands)) {
|
|
91
|
+
out.set(stage[stageCands.indexOf(s)], ghost[ghostCands.indexOf(g)]);
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
function hasOwnTransform(el) {
|
|
96
|
+
if (!(el instanceof SVGGraphicsElement))
|
|
97
|
+
return false;
|
|
98
|
+
const m = el.transform.baseVal.consolidate()?.matrix;
|
|
99
|
+
return !!m && !(m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && m.e === 0 && m.f === 0);
|
|
100
|
+
}
|
|
101
|
+
// Path tweening replaces FLIP only when the path is the whole shape and draws in slide space;
|
|
102
|
+
// otherwise text or siblings in the group would be left behind.
|
|
103
|
+
function soleUntransformedPath(el) {
|
|
104
|
+
const path = el.tagName.toLowerCase() === "path"
|
|
105
|
+
? el
|
|
106
|
+
: el.children.length === 1 && el.children[0].tagName.toLowerCase() === "path"
|
|
107
|
+
? el.children[0]
|
|
108
|
+
: null;
|
|
109
|
+
if (!path || hasOwnTransform(el) || (path !== el && hasOwnTransform(path)))
|
|
110
|
+
return null;
|
|
111
|
+
return path;
|
|
112
|
+
}
|
|
113
|
+
export const morph = async (ctx) => {
|
|
114
|
+
const ghostMatched = collectMatchable(ctx.ghost);
|
|
115
|
+
const stageMatched = collectMatchable(ctx.stage);
|
|
116
|
+
if (ghostMatched.length === 0 || stageMatched.length === 0) {
|
|
117
|
+
const dur = ms(ctx);
|
|
118
|
+
const anims = [
|
|
119
|
+
ctx.ghost.animate([{ opacity: 1 }, { opacity: 0 }], {
|
|
120
|
+
duration: dur,
|
|
121
|
+
easing: STD_EASE,
|
|
122
|
+
fill: "forwards",
|
|
123
|
+
}),
|
|
124
|
+
];
|
|
125
|
+
if (stageMatched.length === 0 && ghostMatched.length > 0) {
|
|
126
|
+
anims.push(ctx.stage.animate([{ opacity: 0.92 }, { opacity: 1 }], {
|
|
127
|
+
duration: dur,
|
|
128
|
+
easing: STD_EASE,
|
|
129
|
+
fill: "forwards",
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
await runAll(anims);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const usedGhostIds = new Set();
|
|
136
|
+
const anims = [];
|
|
137
|
+
const pathTweens = [];
|
|
138
|
+
const dur = ms(ctx);
|
|
139
|
+
const pairs = pairMatchable(ghostMatched, stageMatched);
|
|
140
|
+
for (const s of stageMatched) {
|
|
141
|
+
const match = pairs.get(s);
|
|
142
|
+
if (!match) {
|
|
143
|
+
s.el.style.opacity = "0";
|
|
144
|
+
const a = s.el.animate([{ opacity: 0 }, { opacity: 1 }], {
|
|
145
|
+
duration: dur,
|
|
146
|
+
easing: STD_EASE,
|
|
147
|
+
fill: "forwards",
|
|
148
|
+
});
|
|
149
|
+
anims.push(a);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
usedGhostIds.add(match.id);
|
|
153
|
+
// The stage shape starts on the ghost's geometry and carries the pair from here on.
|
|
154
|
+
match.el.style.visibility = "hidden";
|
|
155
|
+
const pathEl = soleUntransformedPath(s.el);
|
|
156
|
+
const tween = pathEl && soleUntransformedPath(match.el) && s.pathD && match.pathD
|
|
157
|
+
? makeMorphPathTween(match.pathD, s.pathD)
|
|
158
|
+
: null;
|
|
159
|
+
if (pathEl && tween) {
|
|
160
|
+
// The ghost's d is already in slide coordinates, so the tween carries position and size.
|
|
161
|
+
pathTweens.push({ el: pathEl, tween });
|
|
162
|
+
pathEl.setAttribute("d", match.pathD);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
// SVG CSS transforms are in user units and default to transform-box: view-box.
|
|
166
|
+
const svgScale = s.el instanceof SVGGraphicsElement ? (s.el.ownerSVGElement?.getScreenCTM()?.a ?? 1) : 1;
|
|
167
|
+
const dx = (match.rect.left - s.rect.left) / (svgScale || 1);
|
|
168
|
+
const dy = (match.rect.top - s.rect.top) / (svgScale || 1);
|
|
169
|
+
const sx = match.rect.width / Math.max(1, s.rect.width);
|
|
170
|
+
const sy = match.rect.height / Math.max(1, s.rect.height);
|
|
171
|
+
const el = s.el;
|
|
172
|
+
el.style.transformBox = "fill-box";
|
|
173
|
+
el.style.transformOrigin = "top left";
|
|
174
|
+
const a = el.animate([
|
|
175
|
+
{ transform: `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`, opacity: 1 },
|
|
176
|
+
{ transform: "translate(0,0) scale(1,1)", opacity: 1 },
|
|
177
|
+
], { duration: dur, easing: STD_EASE, fill: "forwards" });
|
|
178
|
+
anims.push(a);
|
|
179
|
+
}
|
|
180
|
+
for (const leftover of ghostMatched) {
|
|
181
|
+
if (usedGhostIds.has(leftover.id))
|
|
182
|
+
continue;
|
|
183
|
+
const a = leftover.el.animate([{ opacity: 1 }, { opacity: 0 }], {
|
|
184
|
+
duration: dur,
|
|
185
|
+
easing: STD_EASE,
|
|
186
|
+
fill: "forwards",
|
|
187
|
+
});
|
|
188
|
+
anims.push(a);
|
|
189
|
+
}
|
|
190
|
+
const fadeGhost = ctx.ghost.animate([{ opacity: 1 }, { opacity: 0 }], {
|
|
191
|
+
duration: dur * 0.7,
|
|
192
|
+
delay: dur * 0.3,
|
|
193
|
+
easing: STD_EASE,
|
|
194
|
+
fill: "forwards",
|
|
195
|
+
});
|
|
196
|
+
anims.push(fadeGhost);
|
|
197
|
+
if (pathTweens.length > 0) {
|
|
198
|
+
const start = performance.now();
|
|
199
|
+
await new Promise((resolve) => {
|
|
200
|
+
const step = () => {
|
|
201
|
+
const t = Math.min(1, (performance.now() - start) / Math.max(1, dur));
|
|
202
|
+
for (const pt of pathTweens)
|
|
203
|
+
pt.el.setAttribute("d", pt.tween(t));
|
|
204
|
+
if (t < 1)
|
|
205
|
+
requestAnimationFrame(step);
|
|
206
|
+
else
|
|
207
|
+
resolve();
|
|
208
|
+
};
|
|
209
|
+
requestAnimationFrame(step);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
await runAll(anims);
|
|
213
|
+
for (const s of stageMatched) {
|
|
214
|
+
s.el.style.transform = "";
|
|
215
|
+
s.el.style.opacity = "";
|
|
216
|
+
s.el.style.transformOrigin = "";
|
|
217
|
+
s.el.style.transformBox = "";
|
|
218
|
+
}
|
|
219
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as fx2d from "./2d.js";
|
|
2
|
+
import * as fx3d from "./3d.js";
|
|
3
|
+
import * as fxGl from "./webgl.js";
|
|
4
|
+
import { morph } from "./morph.js";
|
|
5
|
+
const registry = new Map();
|
|
6
|
+
function register(name, fn) {
|
|
7
|
+
registry.set(name, fn);
|
|
8
|
+
}
|
|
9
|
+
register("cut", fx2d.cut);
|
|
10
|
+
register("fade", fx2d.fade);
|
|
11
|
+
register("push", fx2d.push);
|
|
12
|
+
register("cover", fx2d.cover);
|
|
13
|
+
register("uncover", fx2d.uncover);
|
|
14
|
+
register("wipe", fx2d.wipe);
|
|
15
|
+
register("split", fx2d.split);
|
|
16
|
+
register("zoom", fx2d.zoom);
|
|
17
|
+
register("blinds", fx2d.blinds);
|
|
18
|
+
register("checker", fx2d.checker);
|
|
19
|
+
register("randomBar", fx2d.randomBar);
|
|
20
|
+
register("dissolve", fxGl.dissolveWebGL);
|
|
21
|
+
register("newsflash", fx2d.newsflash);
|
|
22
|
+
register("wheel", fx2d.wheel);
|
|
23
|
+
register("wheelReverse", fx2d.wheelReverse);
|
|
24
|
+
register("circle", fx2d.circle);
|
|
25
|
+
register("diamond", fx2d.diamond);
|
|
26
|
+
register("plus", fx2d.plus);
|
|
27
|
+
register("comb", fx2d.comb);
|
|
28
|
+
register("strips", fx2d.strips);
|
|
29
|
+
register("wedge", fx2d.wedge);
|
|
30
|
+
register("fall", fx2d.fall);
|
|
31
|
+
register("reveal", fx2d.reveal);
|
|
32
|
+
register("flash", fx2d.flash);
|
|
33
|
+
register("random", fx2d.random);
|
|
34
|
+
register("glitter", fx2d.glitter);
|
|
35
|
+
register("honeycomb", fx2d.honeycomb);
|
|
36
|
+
register("fracture", fx2d.fracture);
|
|
37
|
+
register("shred", fx2d.shred);
|
|
38
|
+
register("prestige", fx2d.prestige);
|
|
39
|
+
register("prism", fx3d.prism);
|
|
40
|
+
register("cube", fxGl.cubeWebGL);
|
|
41
|
+
register("box", fxGl.boxWebGL);
|
|
42
|
+
register("gallery", fx3d.gallery);
|
|
43
|
+
register("orbit", fx3d.orbit);
|
|
44
|
+
register("conveyor", fx3d.conveyor);
|
|
45
|
+
register("doors", fx3d.doors);
|
|
46
|
+
register("window", fx3d.window);
|
|
47
|
+
register("pageCurl", fxGl.pageCurlWebGL);
|
|
48
|
+
register("pageCurlSingle", fx3d.pageCurlSingle);
|
|
49
|
+
register("wind", fx3d.wind);
|
|
50
|
+
register("crush", fx3d.crush);
|
|
51
|
+
register("pageCurlCss", fx3d.pageCurl);
|
|
52
|
+
register("airplane", fx3d.airplane);
|
|
53
|
+
register("origami", fx3d.origami);
|
|
54
|
+
register("flythrough", fx3d.flythrough);
|
|
55
|
+
register("rotate", fx3d.rotate);
|
|
56
|
+
register("ferris", fx3d.ferris);
|
|
57
|
+
register("peelOff", fx3d.peelOff);
|
|
58
|
+
register("curtains", fx3d.curtains);
|
|
59
|
+
register("drape", fx3d.drape);
|
|
60
|
+
register("ripple", fx3d.ripple);
|
|
61
|
+
register("warp", fx3d.warp);
|
|
62
|
+
register("vortex", fx3d.vortex);
|
|
63
|
+
register("flip", fx3d.flip);
|
|
64
|
+
register("switch", fx3d.switchFx);
|
|
65
|
+
register("morph", morph);
|
|
66
|
+
export function getTransition(kind) {
|
|
67
|
+
return registry.get(kind);
|
|
68
|
+
}
|
|
69
|
+
export function listTransitions() {
|
|
70
|
+
return [...registry.keys()];
|
|
71
|
+
}
|
|
72
|
+
export function registerTransition(kind, fn) {
|
|
73
|
+
register(kind, fn);
|
|
74
|
+
}
|
|
75
|
+
export async function runTransition(ghost, stage, transition) {
|
|
76
|
+
if (transition.kind === "cut" || transition.duration <= 0)
|
|
77
|
+
return;
|
|
78
|
+
const fn = registry.get(transition.kind) ?? registry.get("fade");
|
|
79
|
+
if (!fn)
|
|
80
|
+
return;
|
|
81
|
+
const rect = stage.getBoundingClientRect();
|
|
82
|
+
const ctx = {
|
|
83
|
+
ghost,
|
|
84
|
+
stage,
|
|
85
|
+
duration: transition.duration,
|
|
86
|
+
params: transition.params ?? {},
|
|
87
|
+
transition,
|
|
88
|
+
stageSize: { w: rect.width, h: rect.height },
|
|
89
|
+
};
|
|
90
|
+
await fn(ctx);
|
|
91
|
+
}
|